From 282c486d095ca2f14a8747eace532f11aa6835b7 Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Thu, 26 Jun 2025 17:25:00 -0700 Subject: [PATCH 01/19] Initial commit of adding suspend modifier to DAO functions --- .../org/wikipedia/database/AppDatabase.kt | 1 - .../readinglist/db/ReadingListPageDao.kt | 83 +++++++++---------- 2 files changed, 40 insertions(+), 44 deletions(-) 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/readinglist/db/ReadingListPageDao.kt b/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt index a2e65a94428..d9be2b125d2 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,28 @@ 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) - - @Query("SELECT * FROM ReadingListPage") - fun getAllPages(): List + 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,49 +53,49 @@ 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) { + suspend fun addPagesToList(list: ReadingList, pages: List, queueForSync: Boolean) { addPagesToList(list, pages) if (queueForSync) { ReadingListSyncAdapter.manualSync() @@ -106,7 +103,7 @@ interface ReadingListPageDao { } @Transaction - fun addPagesToList(list: ReadingList, pages: List) { + suspend fun addPagesToList(list: ReadingList, pages: List) { for (page in pages) { insertPageIntoDb(list, page) } @@ -115,7 +112,7 @@ interface ReadingListPageDao { } @Transaction - fun addPagesToListIfNotExist(list: ReadingList, titles: List): List { + suspend fun addPagesToListIfNotExist(list: ReadingList, titles: List): List { val addedTitles = mutableListOf() for (title in titles) { if (getPageByTitle(list, title) != null) { @@ -132,7 +129,7 @@ interface ReadingListPageDao { } @Transaction - fun updatePages(pages: List) { + suspend fun updatePages(pages: List) { for (page in pages) { updateReadingListPage(page) } @@ -149,7 +146,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 +163,16 @@ interface ReadingListPageDao { }.toMutableList()) } - fun pageExistsInList(list: ReadingList, title: PageTitle): Boolean { + suspend fun pageExistsInList(list: ReadingList, title: PageTitle): Boolean { return getPageByTitle(list, title) != null } - fun resetUnsavedPageStatus() { + suspend 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,12 +184,12 @@ interface ReadingListPageDao { SavedPageSyncService.enqueue() } - fun markPageForOffline(page: ReadingListPage, offline: Boolean, forcedSave: Boolean) { + suspend fun markPageForOffline(page: ReadingListPage, offline: Boolean, forcedSave: Boolean) { markPagesForOffline(listOf(page), offline, forcedSave) } @Transaction - fun markPagesForOffline(pages: List, offline: Boolean, forcedSave: Boolean) { + suspend fun markPagesForOffline(pages: List, offline: Boolean, forcedSave: Boolean) { for (page in pages) { if (page.offline == offline && !forcedSave) { continue @@ -206,12 +203,12 @@ interface ReadingListPageDao { 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,7 +221,7 @@ interface ReadingListPageDao { return movedTitles } - private fun movePageToList(sourceList: ReadingList, destList: ReadingList, title: PageTitle) { + private suspend fun movePageToList(sourceList: ReadingList, destList: ReadingList, title: PageTitle) { if (sourceList.id == destList.id) { return } @@ -239,14 +236,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) { + suspend fun addPageToList(list: ReadingList, title: PageTitle, queueForSync: Boolean) { addPageToList(list, title) SavedPageSyncService.enqueue() if (queueForSync) { @@ -255,7 +252,7 @@ interface ReadingListPageDao { } @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 +268,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) { val protoPage = ReadingListPage(title) insertPageIntoDb(list, protoPage) FlowEventBus.post(ArticleSavedOrDeletedEvent(true, protoPage)) } - private fun insertPageIntoDb(list: ReadingList, page: ReadingListPage) { + private suspend fun insertPageIntoDb(list: ReadingList, page: ReadingListPage) { page.listId = list.id page.id = insertReadingListPage(page) } From 30eb36ec4fc998fb8a90254a4915210629809a3f Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Fri, 27 Jun 2025 12:15:50 -0700 Subject: [PATCH 02/19] More updates --- .../okhttp/OfflineCacheInterceptor.kt | 49 +++-- .../wikipedia/offline/db/OfflineObjectDao.kt | 31 +-- .../java/org/wikipedia/page/PageFragment.kt | 7 +- .../readinglist/AddToReadingListDialog.kt | 6 +- .../readinglist/ReadingListBehaviorsUtil.kt | 207 +++++++++++------- .../readinglist/ReadingListFragment.kt | 27 +-- .../ReadingListItemActionsDialog.kt | 18 +- .../ReadingListPreviewSaveDialogView.kt | 12 +- .../readinglist/ReadingListsFragment.kt | 29 ++- .../readinglist/db/ReadingListDao.kt | 35 +-- .../readinglist/db/ReadingListPageDao.kt | 34 +-- .../sync/ReadingListSyncAdapter.kt | 15 +- .../savedpages/SavedPageSyncService.kt | 12 +- .../dev/DeveloperSettingsPreferenceLoader.kt | 57 +++-- .../org/wikipedia/talk/TalkTopicHolder.kt | 148 +++++++------ .../org/wikipedia/talk/TalkTopicsViewModel.kt | 2 +- .../org/wikipedia/talk/db/TalkPageSeenDao.kt | 4 +- 17 files changed, 406 insertions(+), 287 deletions(-) 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/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 89f4a3d226b..976818f9678 100644 --- a/app/src/main/java/org/wikipedia/page/PageFragment.kt +++ b/app/src/main/java/org/wikipedia/page/PageFragment.kt @@ -908,7 +908,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 + ) } } } 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..cce2cea4a85 100644 --- a/app/src/main/java/org/wikipedia/readinglist/ReadingListBehaviorsUtil.kt +++ b/app/src/main/java/org/wikipedia/readinglist/ReadingListBehaviorsUtil.kt @@ -51,7 +51,7 @@ object ReadingListBehaviorsUtil { fun getListsContainPage(readingListPage: ReadingListPage) = allReadingLists.filter { list -> list.pages.any { it.apiTitle == readingListPage.apiTitle } } - fun savePagesForOffline(activity: Activity, selectedPages: List, callback: Callback) { + fun savePagesForOffline(activity: AppCompatActivity, selectedPages: List, callback: Callback) { if (Prefs.isDownloadOnlyOverWiFiEnabled && !DeviceUtil.isOnWiFi) { showMobileDataWarningDialog(activity) { _, _ -> savePagesForOffline(activity, selectedPages, true) @@ -63,25 +63,30 @@ object ReadingListBehaviorsUtil { } } - private fun savePagesForOffline(activity: Activity, selectedPages: List, forcedSave: Boolean) { + private fun savePagesForOffline(activity: AppCompatActivity, selectedPages: List, forcedSave: Boolean) { if (selectedPages.isNotEmpty()) { - for (page in selectedPages) { - resetPageProgress(page) + activity.lifecycleScope.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) { + fun removePagesFromOffline(activity: AppCompatActivity, selectedPages: List, callback: Callback) { if (selectedPages.isNotEmpty()) { - AppDatabase.instance.readingListPageDao().markPagesForOffline(selectedPages, offline = false, forcedSave = false) - showMultiSelectOfflineStateChangeSnackbar(activity, selectedPages, false) - callback.onCompleted() + activity.lifecycleScope.launch(exceptionHandler) { + AppDatabase.instance.readingListPageDao().markPagesForOffline(selectedPages, offline = false, forcedSave = false) + showMultiSelectOfflineStateChangeSnackbar(activity, selectedPages, false) + callback.onCompleted() + } } } - fun deleteReadingList(activity: Activity, readingList: ReadingList?, showDialog: Boolean, callback: Callback) { + fun deleteReadingList(activity: AppCompatActivity, readingList: ReadingList?, showDialog: Boolean, callback: Callback) { if (readingList == null) { return } @@ -89,50 +94,59 @@ 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() } + activity.lifecycleScope.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() + activity.lifecycleScope.launch(exceptionHandler) { + AppDatabase.instance.readingListDao().deleteList(readingList) + AppDatabase.instance.readingListPageDao() + .markPagesForDeletion(readingList, readingList.pages, false) + callback.onCompleted() + } } } - fun deleteReadingLists(activity: Activity, readingLists: List, callback: Callback) { + fun deleteReadingLists(activity: AppCompatActivity, readingLists: List, callback: Callback) { MaterialAlertDialogBuilder(activity) .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) + activity.lifecycleScope.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() - } + activity.lifecycleScope.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() } } @@ -145,7 +159,7 @@ object ReadingListBehaviorsUtil { } } - fun renameReadingList(activity: Activity, readingList: ReadingList?, callback: Callback) { + fun renameReadingList(activity: AppCompatActivity, readingList: ReadingList?, callback: Callback) { if (readingList == null) { return } else if (readingList.isDefault) { @@ -153,26 +167,31 @@ object ReadingListBehaviorsUtil { return } - val tempLists = AppDatabase.instance.readingListDao().getListsWithoutContents() - val existingTitles = ArrayList() - for (list in tempLists) { - existingTitles.add(list.title) + activity.lifecycleScope.launch(exceptionHandler) { + val tempLists = AppDatabase.instance.readingListDao().getListsWithoutContents() + val existingTitles = ArrayList() + for (list in tempLists) { + existingTitles.add(list.title) + } + existingTitles.remove(readingList.title) + + ReadingListTitleDialog.readingListTitleDialog( + activity, readingList.title, readingList.description, existingTitles, + callback = object : ReadingListTitleDialog.Callback { + override fun onSuccess(text: String, description: String) { + activity.lifecycleScope.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) { + private fun showDeletePageFromListsUndoSnackbar(activity: AppCompatActivity, lists: List?, page: ReadingListPage, callback: SnackbarCallback) { if (lists == null) { return } @@ -186,13 +205,15 @@ 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() + activity.lifecycleScope.launch(exceptionHandler) { + AppDatabase.instance.readingListPageDao().addPageToLists(lists, page, true) + callback.onUndoDeleteClicked() + } } .show() } - fun showDeletePagesUndoSnackbar(activity: Activity, readingList: ReadingList?, pages: List, callback: SnackbarCallback) { + fun showDeletePagesUndoSnackbar(activity: AppCompatActivity, readingList: ReadingList?, pages: List, callback: SnackbarCallback) { if (readingList == null) { return } @@ -201,47 +222,59 @@ 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))) + activity.lifecycleScope.launch(exceptionHandler) { + val newPages = ArrayList() + for (page in pages) { + newPages.add(ReadingListPage(ReadingListPage.toPageTitle(page))) + } + 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() } - fun showDeleteListUndoSnackbar(activity: Activity, readingList: ReadingList?, callback: SnackbarCallback) { + fun showDeleteListUndoSnackbar(activity: AppCompatActivity, readingList: ReadingList?, callback: SnackbarCallback) { if (readingList == null) { return } 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))) + activity.lifecycleScope.launch(exceptionHandler) { + val newList = AppDatabase.instance.readingListDao() + .createList(readingList.title, readingList.description) + val newPages = ArrayList() + for (page in readingList.pages) { + newPages.add(ReadingListPage(ReadingListPage.toPageTitle(page))) + } + AppDatabase.instance.readingListPageDao() + .addPagesToList(newList, newPages, true) + callback.onUndoDeleteClicked() } - AppDatabase.instance.readingListPageDao().addPagesToList(newList, newPages, true) - callback.onUndoDeleteClicked() } .show() } - fun showDeleteListsUndoSnackbar(activity: Activity, readingLists: List?, callback: SnackbarCallback) { + fun showDeleteListsUndoSnackbar(activity: AppCompatActivity, readingLists: List?, callback: SnackbarCallback) { if (readingLists == null) { return } 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))) + activity.lifecycleScope.launch(exceptionHandler) { + 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))) + } + AppDatabase.instance.readingListPageDao() + .addPagesToList(newList, newPages, true) } - AppDatabase.instance.readingListPageDao().addPagesToList(newList, newPages, true) } callback.onUndoDeleteClicked() } @@ -287,7 +320,7 @@ object ReadingListBehaviorsUtil { } } - fun toggleOffline(activity: Activity, page: ReadingListPage, callback: Callback) { + fun toggleOffline(activity: AppCompatActivity, page: ReadingListPage, callback: Callback) { resetPageProgress(page) if (Prefs.isDownloadOnlyOverWiFiEnabled && !DeviceUtil.isOnWiFi && !page.offline) { showMobileDataWarningDialog(activity) { _, _ -> @@ -335,11 +368,19 @@ object ReadingListBehaviorsUtil { MoveToReadingListDialog.newInstance(sourceReadingListId, title, source, showDefaultList, listener)) } - private fun toggleOffline(activity: Activity, page: ReadingListPage, forcedSave: Boolean) { - AppDatabase.instance.readingListPageDao().markPageForOffline(page, !page.offline, forcedSave) - FeedbackUtil.showMessage(activity, + private fun toggleOffline(activity: AppCompatActivity, page: ReadingListPage, forcedSave: Boolean) { + + activity.lifecycleScope.launch(exceptionHandler) { + AppDatabase.instance.readingListPageDao() + .markPageForOffline(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 d0488a5d8a9..122ad2491c0 100644 --- a/app/src/main/java/org/wikipedia/readinglist/ReadingListFragment.kt +++ b/app/src/main/java/org/wikipedia/readinglist/ReadingListFragment.kt @@ -313,7 +313,7 @@ class ReadingListFragment : Fragment(), MenuProvider, ReadingListItemActionsDial } R.id.menu_reading_list_save_all_offline -> { readingList?.let { - ReadingListBehaviorsUtil.savePagesForOffline(requireActivity(), it.pages) { + ReadingListBehaviorsUtil.savePagesForOffline(requireActivity() as AppCompatActivity, it.pages) { adapter.notifyDataSetChanged() update() } @@ -322,7 +322,7 @@ class ReadingListFragment : Fragment(), MenuProvider, ReadingListItemActionsDial } R.id.menu_reading_list_remove_all_offline -> { readingList?.let { - ReadingListBehaviorsUtil.removePagesFromOffline(requireActivity(), it.pages) { + ReadingListBehaviorsUtil.removePagesFromOffline(requireActivity() as AppCompatActivity, it.pages) { adapter.notifyDataSetChanged() update() } @@ -577,6 +577,7 @@ 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 @@ -627,7 +628,7 @@ class ReadingListFragment : Fragment(), MenuProvider, ReadingListItemActionsDial if (pages.isNotEmpty()) { AppDatabase.instance.readingListPageDao().markPagesForDeletion(it, pages) it.pages.removeAll(pages) - ReadingListBehaviorsUtil.showDeletePagesUndoSnackbar(requireActivity(), it, pages) { updateReadingListData() } + ReadingListBehaviorsUtil.showDeletePagesUndoSnackbar(requireActivity() as AppCompatActivity, it, pages) { updateReadingListData() } update() } } @@ -655,7 +656,7 @@ class ReadingListFragment : Fragment(), MenuProvider, ReadingListItemActionsDial private fun delete() { readingList?.let { - ReadingListBehaviorsUtil.deleteReadingList(requireActivity(), it, true) { + ReadingListBehaviorsUtil.deleteReadingList(requireActivity() as AppCompatActivity, it, true) { startActivity(MainActivity.newIntent(requireActivity()).putExtra(Constants.INTENT_EXTRA_DELETE_READING_LIST, it.title)) requireActivity().finish() } @@ -920,14 +921,14 @@ class ReadingListFragment : Fragment(), MenuProvider, ReadingListItemActionsDial } override fun onSaveAllOffline(readingList: ReadingList) { - ReadingListBehaviorsUtil.savePagesForOffline(requireActivity(), readingList.pages) { + ReadingListBehaviorsUtil.savePagesForOffline(requireActivity() as AppCompatActivity, readingList.pages) { adapter.notifyDataSetChanged() update() } } override fun onRemoveAllOffline(readingList: ReadingList) { - ReadingListBehaviorsUtil.removePagesFromOffline(requireActivity(), readingList.pages) { + ReadingListBehaviorsUtil.removePagesFromOffline(requireActivity() as AppCompatActivity, readingList.pages) { adapter.notifyDataSetChanged() update() } @@ -982,18 +983,18 @@ class ReadingListFragment : Fragment(), MenuProvider, ReadingListItemActionsDial } override fun onDelete(readingList: ReadingList) { - ReadingListBehaviorsUtil.deleteReadingList(requireActivity(), readingList, true) { - ReadingListBehaviorsUtil.showDeleteListUndoSnackbar(requireActivity(), readingList) { setSearchQuery() } + ReadingListBehaviorsUtil.deleteReadingList(requireActivity() as AppCompatActivity, readingList, true) { + ReadingListBehaviorsUtil.showDeleteListUndoSnackbar(requireActivity() as AppCompatActivity, readingList) { setSearchQuery() } setSearchQuery() } } override fun onSaveAllOffline(readingList: ReadingList) { - ReadingListBehaviorsUtil.savePagesForOffline(requireActivity(), readingList.pages) { setSearchQuery() } + ReadingListBehaviorsUtil.savePagesForOffline(requireActivity() as AppCompatActivity, readingList.pages) { setSearchQuery() } } override fun onRemoveAllOffline(readingList: ReadingList) { - ReadingListBehaviorsUtil.removePagesFromOffline(requireActivity(), readingList.pages) { setSearchQuery() } + ReadingListBehaviorsUtil.removePagesFromOffline(requireActivity() as AppCompatActivity, readingList.pages) { setSearchQuery() } } override fun onShare(readingList: ReadingList) { @@ -1038,7 +1039,7 @@ class ReadingListFragment : Fragment(), MenuProvider, ReadingListItemActionsDial if (it.saving) { Toast.makeText(context, R.string.reading_list_article_save_in_progress, Toast.LENGTH_LONG).show() } else { - ReadingListBehaviorsUtil.toggleOffline(requireActivity(), item) { + ReadingListBehaviorsUtil.toggleOffline(requireActivity() as AppCompatActivity, item) { adapter.notifyDataSetChanged() update() } @@ -1103,7 +1104,7 @@ class ReadingListFragment : Fragment(), MenuProvider, ReadingListItemActionsDial return true } R.id.menu_remove_from_offline -> { - ReadingListBehaviorsUtil.removePagesFromOffline(requireActivity(), selectedPages) { + ReadingListBehaviorsUtil.removePagesFromOffline(requireActivity() as AppCompatActivity, selectedPages) { adapter.notifyDataSetChanged() update() } @@ -1111,7 +1112,7 @@ class ReadingListFragment : Fragment(), MenuProvider, ReadingListItemActionsDial return true } R.id.menu_save_for_offline -> { - ReadingListBehaviorsUtil.savePagesForOffline(requireActivity(), selectedPages) { + ReadingListBehaviorsUtil.savePagesForOffline(requireActivity() as AppCompatActivity, selectedPages) { adapter.notifyDataSetChanged() update() } 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/ReadingListsFragment.kt b/app/src/main/java/org/wikipedia/readinglist/ReadingListsFragment.kt index e8cc51c36c1..0254207614e 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 @@ -231,8 +232,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() } @@ -511,25 +516,25 @@ 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) } } override fun onDelete(readingList: ReadingList) { - ReadingListBehaviorsUtil.deleteReadingList(requireActivity(), readingList, true) { - ReadingListBehaviorsUtil.showDeleteListUndoSnackbar(requireActivity(), readingList) { updateLists() } + ReadingListBehaviorsUtil.deleteReadingList(requireActivity() as AppCompatActivity, readingList, true) { + ReadingListBehaviorsUtil.showDeleteListUndoSnackbar(requireActivity() as AppCompatActivity, readingList) { updateLists() } updateLists() } } override fun onSaveAllOffline(readingList: ReadingList) { - ReadingListBehaviorsUtil.savePagesForOffline(requireActivity(), readingList.pages) { updateLists(currentSearchQuery, true) } + ReadingListBehaviorsUtil.savePagesForOffline(requireActivity() as AppCompatActivity, readingList.pages) { updateLists(currentSearchQuery, true) } } override fun onRemoveAllOffline(readingList: ReadingList) { - ReadingListBehaviorsUtil.removePagesFromOffline(requireActivity(), readingList.pages) { updateLists(currentSearchQuery, true) } + ReadingListBehaviorsUtil.removePagesFromOffline(requireActivity() as AppCompatActivity, readingList.pages) { updateLists(currentSearchQuery, true) } } override fun onSelectList(readingList: ReadingList) { @@ -600,7 +605,7 @@ class ReadingListsFragment : Fragment(), SortReadingListsDialog.Callback, Readin if (it.saving) { Toast.makeText(context, R.string.reading_list_article_save_in_progress, Toast.LENGTH_LONG).show() } else { - ReadingListBehaviorsUtil.toggleOffline(requireActivity(), it) { adapter.notifyDataSetChanged() } + ReadingListBehaviorsUtil.toggleOffline(requireActivity() as AppCompatActivity, it) { adapter.notifyDataSetChanged() } } } } @@ -617,8 +622,8 @@ class ReadingListsFragment : Fragment(), SortReadingListsDialog.Callback, Readin requireActivity().intent.removeExtra(Constants.INTENT_EXTRA_DELETE_READING_LIST) displayedLists.forEach { if (it is ReadingList && it.title == titleToDelete) { - ReadingListBehaviorsUtil.deleteReadingList(requireActivity(), it, false) { - ReadingListBehaviorsUtil.showDeleteListUndoSnackbar(requireActivity(), it) { updateLists() } + ReadingListBehaviorsUtil.deleteReadingList(requireActivity() as AppCompatActivity, it, false) { + ReadingListBehaviorsUtil.showDeleteListUndoSnackbar(requireActivity() as AppCompatActivity, it) { updateLists() } updateLists() } } @@ -704,8 +709,8 @@ class ReadingListsFragment : Fragment(), SortReadingListsDialog.Callback, Readin override fun onDeleteSelected() { selectedLists.let { - ReadingListBehaviorsUtil.deleteReadingLists(requireActivity(), it) { - ReadingListBehaviorsUtil.showDeleteListsUndoSnackbar(requireActivity(), it) { updateLists() } + ReadingListBehaviorsUtil.deleteReadingLists(requireActivity() as AppCompatActivity, it) { + ReadingListBehaviorsUtil.showDeleteListsUndoSnackbar(requireActivity() as AppCompatActivity, it) { updateLists() } finishActionMode() updateLists() } 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 d9be2b125d2..6b5443f061e 100644 --- a/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt +++ b/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt @@ -29,6 +29,9 @@ interface ReadingListPageDao { @Update(onConflict = OnConflictStrategy.REPLACE) suspend fun updateReadingListPage(page: ReadingListPage) + @Update(onConflict = OnConflictStrategy.REPLACE) + suspend fun updateReadingListPages(pages: List) + @Delete suspend fun deleteReadingListPage(page: ReadingListPage) @@ -95,20 +98,16 @@ interface ReadingListPageDao { list.pages.addAll(getPagesByListId(list.id, ReadingListPage.STATUS_QUEUE_FOR_DELETE)) } - suspend fun addPagesToList(list: ReadingList, pages: List, queueForSync: Boolean) { - addPagesToList(list, pages) - if (queueForSync) { - ReadingListSyncAdapter.manualSync() - } - } - @Transaction - suspend 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 @@ -128,17 +127,6 @@ interface ReadingListPageDao { return addedTitles } - @Transaction - suspend 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(), @@ -163,14 +151,6 @@ interface ReadingListPageDao { }.toMutableList()) } - suspend fun pageExistsInList(list: ReadingList, title: PageTitle): Boolean { - return getPageByTitle(list, title) != null - } - - suspend fun resetUnsavedPageStatus() { - updateStatus(ReadingListPage.STATUS_SAVED, ReadingListPage.STATUS_QUEUE_FOR_SAVE, false) - } - @Transaction suspend fun markPagesForDeletion(list: ReadingList, pages: List, queueForSync: Boolean = true) { for (page in pages) { 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..290906875e5 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 5634426c00f..2568b23d2bd 100644 --- a/app/src/main/java/org/wikipedia/settings/dev/DeveloperSettingsPreferenceLoader.kt +++ b/app/src/main/java/org/wikipedia/settings/dev/DeveloperSettingsPreferenceLoader.kt @@ -112,7 +112,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 { @@ -247,30 +254,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..48e56cec139 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, @@ -32,78 +36,82 @@ class TalkTopicHolder internal constructor( private lateinit var threadItem: ThreadItem fun bindItem(item: ThreadItem) { - item.seen = viewModel.topicSeen(item) - threadItem = item - 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) - - // setting tag for swipe action text - if (!threadItem.seen) { - itemView.setTag(R.string.tag_text_key, context.getString(R.string.talk_list_item_swipe_mark_as_read)) - itemView.setTag(R.string.tag_icon_key, R.drawable.ic_outline_drafts_24) - } else { - itemView.setTag(R.string.tag_text_key, context.getString(R.string.talk_list_item_swipe_mark_as_unread)) - itemView.setTag(R.string.tag_icon_key, R.drawable.ic_outline_email_24) - } + context.lifecycleScope.launch(CoroutineExceptionHandler { _, throwable -> + L.e(throwable) + }) { + item.seen = viewModel.topicSeen(item) + threadItem = item + 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@TalkTopicHolder) + + // setting tag for swipe action text + if (!threadItem.seen) { + itemView.setTag(R.string.tag_text_key, context.getString(R.string.talk_list_item_swipe_mark_as_read)) + itemView.setTag(R.string.tag_icon_key, R.drawable.ic_outline_drafts_24) + } else { + itemView.setTag(R.string.tag_text_key, context.getString(R.string.talk_list_item_swipe_mark_as_unread)) + itemView.setTag(R.string.tag_icon_key, R.drawable.ic_outline_email_24) + } - binding.topicOverflowMenu.setOnClickListener { - showOverflowMenu(it) - } + binding.topicOverflowMenu.setOnClickListener { + showOverflowMenu(it) + } - val allReplies = threadItem.allReplies.toList() - - if (allReplies.isEmpty()) { - binding.topicUserIcon.isVisible = false - binding.topicUsername.isVisible = false - binding.topicReplyIcon.isVisible = false - binding.topicReplyNumber.isVisible = false - binding.topicLastCommentDate.isVisible = false - binding.topicContentText.isVisible = false - val isHeaderTemplate = TalkTopicActivity.isHeaderTemplate(threadItem) - binding.otherContentContainer.isVisible = isHeaderTemplate - if (isHeaderTemplate) { - StringUtil.setHighlightedAndBoldenedText(binding.otherContentText, - RichTextUtil.stripHtml(StringUtil.removeStyleTags(threadItem.othercontent)).trim().replace("\n", " "), - viewModel.currentSearchQuery) + val allReplies = threadItem.allReplies.toList() + + if (allReplies.isEmpty()) { + binding.topicUserIcon.isVisible = false + binding.topicUsername.isVisible = false + binding.topicReplyIcon.isVisible = false + binding.topicReplyNumber.isVisible = false + binding.topicLastCommentDate.isVisible = false + binding.topicContentText.isVisible = false + val isHeaderTemplate = TalkTopicActivity.isHeaderTemplate(threadItem) + binding.otherContentContainer.isVisible = isHeaderTemplate + if (isHeaderTemplate) { + StringUtil.setHighlightedAndBoldenedText(binding.otherContentText, + RichTextUtil.stripHtml(StringUtil.removeStyleTags(threadItem.othercontent)).trim().replace("\n", " "), + viewModel.currentSearchQuery) + } + return@launch } - return + binding.otherContentContainer.isVisible = false + + // Last comment + binding.topicContentText.isVisible = viewModel.pageTitle.namespace() == Namespace.USER_TALK + StringUtil.setHighlightedAndBoldenedText(binding.topicContentText, + RichTextUtil.stripHtml(allReplies.last().html).trim().replace("\n", " "), + viewModel.currentSearchQuery) + binding.topicContentText.setTextColor(ResourceUtil.getThemedColor(context, if (threadItem.seen) android.R.attr.textColorTertiary else R.attr.primary_color)) + + // Username with involved user number exclude the author + val usersInvolved = allReplies.map { it.author }.distinct().size - 1 + val usernameText = allReplies.maxByOrNull { it.date ?: Date() }?.author.orEmpty() + (if (usersInvolved > 1) " +$usersInvolved" else "") + val usernameColor = if (threadItem.seen) R.attr.inactive_color else R.attr.progressive_color + StringUtil.setHighlightedAndBoldenedText(binding.topicUsername, usernameText, viewModel.currentSearchQuery) + binding.topicUserIcon.isVisible = viewModel.pageTitle.namespace() == Namespace.USER_TALK + binding.topicUsername.isVisible = viewModel.pageTitle.namespace() == Namespace.USER_TALK + binding.topicUsername.setTextColor(ResourceUtil.getThemedColor(context, usernameColor)) + ImageViewCompat.setImageTintList(binding.topicUserIcon, ResourceUtil.getThemedColorStateList(context, usernameColor)) + + // Amount of replies, exclude the topic in replies[]. + val replyNumber = allReplies.size - 1 + val replyNumberColor = if (threadItem.seen) R.attr.inactive_color else R.attr.placeholder_color + binding.topicReplyNumber.isVisible = replyNumber > 0 + binding.topicReplyIcon.isVisible = replyNumber > 0 + binding.topicReplyNumber.text = replyNumber.toString() + binding.topicReplyNumber.setTextColor(ResourceUtil.getThemedColor(context, replyNumberColor)) + ImageViewCompat.setImageTintList(binding.topicReplyIcon, ResourceUtil.getThemedColorStateList(context, replyNumberColor)) + + // Last comment date + val lastCommentDate = allReplies.mapNotNull { it.date }.maxOrNull()?.run { DateUtil.getDateAndTime(context, this) } + val lastCommentColor = if (threadItem.seen) R.attr.inactive_color else R.attr.placeholder_color + binding.topicLastCommentDate.text = lastCommentDate + binding.topicLastCommentDate.isVisible = lastCommentDate != null + binding.topicLastCommentDate.setTextColor(ResourceUtil.getThemedColor(context, lastCommentColor)) } - binding.otherContentContainer.isVisible = false - - // Last comment - binding.topicContentText.isVisible = viewModel.pageTitle.namespace() == Namespace.USER_TALK - StringUtil.setHighlightedAndBoldenedText(binding.topicContentText, - RichTextUtil.stripHtml(allReplies.last().html).trim().replace("\n", " "), - viewModel.currentSearchQuery) - binding.topicContentText.setTextColor(ResourceUtil.getThemedColor(context, if (threadItem.seen) android.R.attr.textColorTertiary else R.attr.primary_color)) - - // Username with involved user number exclude the author - val usersInvolved = allReplies.map { it.author }.distinct().size - 1 - val usernameText = allReplies.maxByOrNull { it.date ?: Date() }?.author.orEmpty() + (if (usersInvolved > 1) " +$usersInvolved" else "") - val usernameColor = if (threadItem.seen) R.attr.inactive_color else R.attr.progressive_color - StringUtil.setHighlightedAndBoldenedText(binding.topicUsername, usernameText, viewModel.currentSearchQuery) - binding.topicUserIcon.isVisible = viewModel.pageTitle.namespace() == Namespace.USER_TALK - binding.topicUsername.isVisible = viewModel.pageTitle.namespace() == Namespace.USER_TALK - binding.topicUsername.setTextColor(ResourceUtil.getThemedColor(context, usernameColor)) - ImageViewCompat.setImageTintList(binding.topicUserIcon, ResourceUtil.getThemedColorStateList(context, usernameColor)) - - // Amount of replies, exclude the topic in replies[]. - val replyNumber = allReplies.size - 1 - val replyNumberColor = if (threadItem.seen) R.attr.inactive_color else R.attr.placeholder_color - binding.topicReplyNumber.isVisible = replyNumber > 0 - binding.topicReplyIcon.isVisible = replyNumber > 0 - binding.topicReplyNumber.text = replyNumber.toString() - binding.topicReplyNumber.setTextColor(ResourceUtil.getThemedColor(context, replyNumberColor)) - ImageViewCompat.setImageTintList(binding.topicReplyIcon, ResourceUtil.getThemedColorStateList(context, replyNumberColor)) - - // Last comment date - val lastCommentDate = allReplies.mapNotNull { it.date }.maxOrNull()?.run { DateUtil.getDateAndTime(context, this) } - val lastCommentColor = if (threadItem.seen) R.attr.inactive_color else R.attr.placeholder_color - binding.topicLastCommentDate.text = lastCommentDate - binding.topicLastCommentDate.isVisible = lastCommentDate != null - binding.topicLastCommentDate.setTextColor(ResourceUtil.getThemedColor(context, lastCommentColor)) } override fun onClick(v: 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..875d4c5a8e4 100644 --- a/app/src/main/java/org/wikipedia/talk/TalkTopicsViewModel.kt +++ b/app/src/main/java/org/wikipedia/talk/TalkTopicsViewModel.kt @@ -141,7 +141,7 @@ class TalkTopicsViewModel(var pageTitle: PageTitle) : ViewModel() { } } - fun topicSeen(threadItem: ThreadItem?): Boolean { + suspend fun topicSeen(threadItem: ThreadItem?): Boolean { return threadSha(threadItem)?.run { talkPageDao.getTalkPageSeen(this) != null } ?: false } 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..40d13e05a86 100644 --- a/app/src/main/java/org/wikipedia/talk/db/TalkPageSeenDao.kt +++ b/app/src/main/java/org/wikipedia/talk/db/TalkPageSeenDao.kt @@ -12,10 +12,10 @@ 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(): Flow> @Query("DELETE FROM TalkPageSeen WHERE sha = :sha") suspend fun deleteTalkPageSeen(sha: String) From 14e57c790e8109717101ab1fca8519546ab5c900 Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Fri, 27 Jun 2025 13:32:13 -0700 Subject: [PATCH 03/19] fix errors --- .../readinglist/ReadingListFragment.kt | 55 ++++++++++++++----- .../ReadingListFragmentViewModel.kt | 32 +++++++++++ .../sync/ReadingListSyncAdapter.kt | 2 +- 3 files changed, 75 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/org/wikipedia/readinglist/ReadingListFragment.kt b/app/src/main/java/org/wikipedia/readinglist/ReadingListFragment.kt index a064cb9ce78..983a5662dca 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() as AppCompatActivity, 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() } } @@ -586,9 +622,8 @@ class ReadingListFragment : Fragment(), MenuProvider, ReadingListItemActionsDial 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) + viewModel.saveReadingList(it) + Prefs.readingListRecentReceivedId = it.id if (isRecommendedList) { @@ -626,13 +661,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() as AppCompatActivity, it, pages) { updateReadingListData() } - update() - } + viewModel.deleteSelectedPages(it, selectedPages) } } @@ -981,7 +1010,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/sync/ReadingListSyncAdapter.kt b/app/src/main/java/org/wikipedia/readinglist/sync/ReadingListSyncAdapter.kt index 290906875e5..2d70caa705d 100644 --- a/app/src/main/java/org/wikipedia/readinglist/sync/ReadingListSyncAdapter.kt +++ b/app/src/main/java/org/wikipedia/readinglist/sync/ReadingListSyncAdapter.kt @@ -457,7 +457,7 @@ class ReadingListSyncAdapter(context: Context, params: WorkerParameters) : Corou if (localPage == null) { localPage = ReadingListPage(pageTitleFromRemoteEntry(remotePage)) localPage.listId = listForPage.id - updateOnly = AppDatabase.instance.readingListPageDao().getPageByTitle(listForPage, remoteTitle)!= null + updateOnly = AppDatabase.instance.readingListPageDao().getPageByTitle(listForPage, remoteTitle) != null } localPage.remoteId = remotePage.id if (updateOnly) { From 9a1d2293dfa07567e4e4fdccc0e4c93003824712 Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Fri, 27 Jun 2025 15:54:55 -0700 Subject: [PATCH 04/19] More updates regarding syntax fixes --- .../database/UpgradeFromPreRoomTest.kt | 3 +- .../ReadingListsExportImportHelper.kt | 22 +++++---- .../readinglist/ReadingListsFragment.kt | 2 +- .../RemoveFromReadingListsDialog.kt | 46 ++++++++++++------- .../org/wikipedia/talk/db/TalkPageSeenDao.kt | 3 +- 5 files changed, 46 insertions(+), 30 deletions(-) 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/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 0254207614e..0f8617e56d8 100644 --- a/app/src/main/java/org/wikipedia/readinglist/ReadingListsFragment.kt +++ b/app/src/main/java/org/wikipedia/readinglist/ReadingListsFragment.kt @@ -867,7 +867,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..9fd6894ac7a 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 androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.coroutines.CoroutineExceptionHandler +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: AppCompatActivity, 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]) + activity.lifecycleScope.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: AppCompatActivity, 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 + activity.lifecycleScope.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/talk/db/TalkPageSeenDao.kt b/app/src/main/java/org/wikipedia/talk/db/TalkPageSeenDao.kt index 40d13e05a86..6534c3cd2bf 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 { @@ -15,7 +14,7 @@ interface TalkPageSeenDao { suspend fun getTalkPageSeen(sha: String): TalkPageSeen? @Query("SELECT * FROM TalkPageSeen") - suspend fun getAll(): Flow> + suspend fun getAll(): List @Query("DELETE FROM TalkPageSeen WHERE sha = :sha") suspend fun deleteTalkPageSeen(sha: String) From 84710be585df82572bd2c3828e5e27e47ea62703 Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Fri, 27 Jun 2025 16:15:32 -0700 Subject: [PATCH 05/19] tune up --- .../readinglist/ReadingListBehaviorsUtil.kt | 27 ++++++------------- .../readinglist/ReadingListFragment.kt | 10 ------- 2 files changed, 8 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/org/wikipedia/readinglist/ReadingListBehaviorsUtil.kt b/app/src/main/java/org/wikipedia/readinglist/ReadingListBehaviorsUtil.kt index cce2cea4a85..8e30c3d686b 100644 --- a/app/src/main/java/org/wikipedia/readinglist/ReadingListBehaviorsUtil.kt +++ b/app/src/main/java/org/wikipedia/readinglist/ReadingListBehaviorsUtil.kt @@ -168,12 +168,9 @@ object ReadingListBehaviorsUtil { } activity.lifecycleScope.launch(exceptionHandler) { - val tempLists = AppDatabase.instance.readingListDao().getListsWithoutContents() - val existingTitles = ArrayList() - for (list in tempLists) { - existingTitles.add(list.title) - } - existingTitles.remove(readingList.title) + val existingTitles = AppDatabase.instance.readingListDao().getListsWithoutContents() + .map { it.title } + .filterNot { it == readingList.title } ReadingListTitleDialog.readingListTitleDialog( activity, readingList.title, readingList.description, existingTitles, @@ -223,12 +220,8 @@ object ReadingListBehaviorsUtil { pages.size, pages.size, readingList.title)) .setAction(R.string.reading_list_item_delete_undo) { activity.lifecycleScope.launch(exceptionHandler) { - val newPages = ArrayList() - for (page in pages) { - newPages.add(ReadingListPage(ReadingListPage.toPageTitle(page))) - } - AppDatabase.instance.readingListPageDao() - .addPagesToList(readingList, newPages, true) + val newPages = pages.map { ReadingListPage(ReadingListPage.toPageTitle(it)) } + AppDatabase.instance.readingListPageDao().addPagesToList(readingList, newPages, true) readingList.pages.addAll(newPages) callback.onUndoDeleteClicked() } @@ -245,10 +238,7 @@ object ReadingListBehaviorsUtil { activity.lifecycleScope.launch(exceptionHandler) { val newList = AppDatabase.instance.readingListDao() .createList(readingList.title, readingList.description) - val newPages = ArrayList() - for (page in readingList.pages) { - newPages.add(ReadingListPage(ReadingListPage.toPageTitle(page))) - } + val newPages = readingList.pages.map { ReadingListPage(ReadingListPage.toPageTitle(it)) } AppDatabase.instance.readingListPageDao() .addPagesToList(newList, newPages, true) callback.onUndoDeleteClicked() @@ -268,9 +258,8 @@ object ReadingListBehaviorsUtil { 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))) + val newPages = it.pages.map { page -> + ReadingListPage(ReadingListPage.toPageTitle(page)) } AppDatabase.instance.readingListPageDao() .addPagesToList(newList, newPages, true) diff --git a/app/src/main/java/org/wikipedia/readinglist/ReadingListFragment.kt b/app/src/main/java/org/wikipedia/readinglist/ReadingListFragment.kt index 983a5662dca..a726e11067a 100644 --- a/app/src/main/java/org/wikipedia/readinglist/ReadingListFragment.kt +++ b/app/src/main/java/org/wikipedia/readinglist/ReadingListFragment.kt @@ -623,16 +623,6 @@ class ReadingListFragment : Fragment(), MenuProvider, ReadingListItemActionsDial it.description = null } viewModel.saveReadingList(it) - - 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() } .setNegativeButton(R.string.reading_lists_preview_save_dialog_cancel, null) .create() From 73dfef11f032c85a59f7532898b8cea34d4b8283 Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Fri, 27 Jun 2025 16:34:17 -0700 Subject: [PATCH 06/19] Seen vs unseen --- .../org/wikipedia/talk/TalkTopicHolder.kt | 140 +++++++++--------- .../org/wikipedia/talk/TalkTopicsViewModel.kt | 14 +- 2 files changed, 80 insertions(+), 74 deletions(-) diff --git a/app/src/main/java/org/wikipedia/talk/TalkTopicHolder.kt b/app/src/main/java/org/wikipedia/talk/TalkTopicHolder.kt index 48e56cec139..2b46191dad1 100644 --- a/app/src/main/java/org/wikipedia/talk/TalkTopicHolder.kt +++ b/app/src/main/java/org/wikipedia/talk/TalkTopicHolder.kt @@ -36,82 +36,78 @@ class TalkTopicHolder internal constructor( private lateinit var threadItem: ThreadItem fun bindItem(item: ThreadItem) { - context.lifecycleScope.launch(CoroutineExceptionHandler { _, throwable -> - L.e(throwable) - }) { - item.seen = viewModel.topicSeen(item) - threadItem = item - 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@TalkTopicHolder) - - // setting tag for swipe action text - if (!threadItem.seen) { - itemView.setTag(R.string.tag_text_key, context.getString(R.string.talk_list_item_swipe_mark_as_read)) - itemView.setTag(R.string.tag_icon_key, R.drawable.ic_outline_drafts_24) - } else { - itemView.setTag(R.string.tag_text_key, context.getString(R.string.talk_list_item_swipe_mark_as_unread)) - itemView.setTag(R.string.tag_icon_key, R.drawable.ic_outline_email_24) - } + item.seen = viewModel.topicSeen(item) + threadItem = item + 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@TalkTopicHolder) + + // setting tag for swipe action text + if (!threadItem.seen) { + itemView.setTag(R.string.tag_text_key, context.getString(R.string.talk_list_item_swipe_mark_as_read)) + itemView.setTag(R.string.tag_icon_key, R.drawable.ic_outline_drafts_24) + } else { + itemView.setTag(R.string.tag_text_key, context.getString(R.string.talk_list_item_swipe_mark_as_unread)) + itemView.setTag(R.string.tag_icon_key, R.drawable.ic_outline_email_24) + } - binding.topicOverflowMenu.setOnClickListener { - showOverflowMenu(it) - } + binding.topicOverflowMenu.setOnClickListener { + showOverflowMenu(it) + } - val allReplies = threadItem.allReplies.toList() - - if (allReplies.isEmpty()) { - binding.topicUserIcon.isVisible = false - binding.topicUsername.isVisible = false - binding.topicReplyIcon.isVisible = false - binding.topicReplyNumber.isVisible = false - binding.topicLastCommentDate.isVisible = false - binding.topicContentText.isVisible = false - val isHeaderTemplate = TalkTopicActivity.isHeaderTemplate(threadItem) - binding.otherContentContainer.isVisible = isHeaderTemplate - if (isHeaderTemplate) { - StringUtil.setHighlightedAndBoldenedText(binding.otherContentText, - RichTextUtil.stripHtml(StringUtil.removeStyleTags(threadItem.othercontent)).trim().replace("\n", " "), - viewModel.currentSearchQuery) - } - return@launch + val allReplies = threadItem.allReplies.toList() + + if (allReplies.isEmpty()) { + binding.topicUserIcon.isVisible = false + binding.topicUsername.isVisible = false + binding.topicReplyIcon.isVisible = false + binding.topicReplyNumber.isVisible = false + binding.topicLastCommentDate.isVisible = false + binding.topicContentText.isVisible = false + val isHeaderTemplate = TalkTopicActivity.isHeaderTemplate(threadItem) + binding.otherContentContainer.isVisible = isHeaderTemplate + if (isHeaderTemplate) { + StringUtil.setHighlightedAndBoldenedText(binding.otherContentText, + RichTextUtil.stripHtml(StringUtil.removeStyleTags(threadItem.othercontent)).trim().replace("\n", " "), + viewModel.currentSearchQuery) } - binding.otherContentContainer.isVisible = false - - // Last comment - binding.topicContentText.isVisible = viewModel.pageTitle.namespace() == Namespace.USER_TALK - StringUtil.setHighlightedAndBoldenedText(binding.topicContentText, - RichTextUtil.stripHtml(allReplies.last().html).trim().replace("\n", " "), - viewModel.currentSearchQuery) - binding.topicContentText.setTextColor(ResourceUtil.getThemedColor(context, if (threadItem.seen) android.R.attr.textColorTertiary else R.attr.primary_color)) - - // Username with involved user number exclude the author - val usersInvolved = allReplies.map { it.author }.distinct().size - 1 - val usernameText = allReplies.maxByOrNull { it.date ?: Date() }?.author.orEmpty() + (if (usersInvolved > 1) " +$usersInvolved" else "") - val usernameColor = if (threadItem.seen) R.attr.inactive_color else R.attr.progressive_color - StringUtil.setHighlightedAndBoldenedText(binding.topicUsername, usernameText, viewModel.currentSearchQuery) - binding.topicUserIcon.isVisible = viewModel.pageTitle.namespace() == Namespace.USER_TALK - binding.topicUsername.isVisible = viewModel.pageTitle.namespace() == Namespace.USER_TALK - binding.topicUsername.setTextColor(ResourceUtil.getThemedColor(context, usernameColor)) - ImageViewCompat.setImageTintList(binding.topicUserIcon, ResourceUtil.getThemedColorStateList(context, usernameColor)) - - // Amount of replies, exclude the topic in replies[]. - val replyNumber = allReplies.size - 1 - val replyNumberColor = if (threadItem.seen) R.attr.inactive_color else R.attr.placeholder_color - binding.topicReplyNumber.isVisible = replyNumber > 0 - binding.topicReplyIcon.isVisible = replyNumber > 0 - binding.topicReplyNumber.text = replyNumber.toString() - binding.topicReplyNumber.setTextColor(ResourceUtil.getThemedColor(context, replyNumberColor)) - ImageViewCompat.setImageTintList(binding.topicReplyIcon, ResourceUtil.getThemedColorStateList(context, replyNumberColor)) - - // Last comment date - val lastCommentDate = allReplies.mapNotNull { it.date }.maxOrNull()?.run { DateUtil.getDateAndTime(context, this) } - val lastCommentColor = if (threadItem.seen) R.attr.inactive_color else R.attr.placeholder_color - binding.topicLastCommentDate.text = lastCommentDate - binding.topicLastCommentDate.isVisible = lastCommentDate != null - binding.topicLastCommentDate.setTextColor(ResourceUtil.getThemedColor(context, lastCommentColor)) + return } + binding.otherContentContainer.isVisible = false + + // Last comment + binding.topicContentText.isVisible = viewModel.pageTitle.namespace() == Namespace.USER_TALK + StringUtil.setHighlightedAndBoldenedText(binding.topicContentText, + RichTextUtil.stripHtml(allReplies.last().html).trim().replace("\n", " "), + viewModel.currentSearchQuery) + binding.topicContentText.setTextColor(ResourceUtil.getThemedColor(context, if (threadItem.seen) android.R.attr.textColorTertiary else R.attr.primary_color)) + + // Username with involved user number exclude the author + val usersInvolved = allReplies.map { it.author }.distinct().size - 1 + val usernameText = allReplies.maxByOrNull { it.date ?: Date() }?.author.orEmpty() + (if (usersInvolved > 1) " +$usersInvolved" else "") + val usernameColor = if (threadItem.seen) R.attr.inactive_color else R.attr.progressive_color + StringUtil.setHighlightedAndBoldenedText(binding.topicUsername, usernameText, viewModel.currentSearchQuery) + binding.topicUserIcon.isVisible = viewModel.pageTitle.namespace() == Namespace.USER_TALK + binding.topicUsername.isVisible = viewModel.pageTitle.namespace() == Namespace.USER_TALK + binding.topicUsername.setTextColor(ResourceUtil.getThemedColor(context, usernameColor)) + ImageViewCompat.setImageTintList(binding.topicUserIcon, ResourceUtil.getThemedColorStateList(context, usernameColor)) + + // Amount of replies, exclude the topic in replies[]. + val replyNumber = allReplies.size - 1 + val replyNumberColor = if (threadItem.seen) R.attr.inactive_color else R.attr.placeholder_color + binding.topicReplyNumber.isVisible = replyNumber > 0 + binding.topicReplyIcon.isVisible = replyNumber > 0 + binding.topicReplyNumber.text = replyNumber.toString() + binding.topicReplyNumber.setTextColor(ResourceUtil.getThemedColor(context, replyNumberColor)) + ImageViewCompat.setImageTintList(binding.topicReplyIcon, ResourceUtil.getThemedColorStateList(context, replyNumberColor)) + + // Last comment date + val lastCommentDate = allReplies.mapNotNull { it.date }.maxOrNull()?.run { DateUtil.getDateAndTime(context, this) } + val lastCommentColor = if (threadItem.seen) R.attr.inactive_color else R.attr.placeholder_color + binding.topicLastCommentDate.text = lastCommentDate + binding.topicLastCommentDate.isVisible = lastCommentDate != null + binding.topicLastCommentDate.setTextColor(ResourceUtil.getThemedColor(context, lastCommentColor)) } override fun onClick(v: View?) { diff --git a/app/src/main/java/org/wikipedia/talk/TalkTopicsViewModel.kt b/app/src/main/java/org/wikipedia/talk/TalkTopicsViewModel.kt index 875d4c5a8e4..fc0d144e9ef 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 @@ -103,6 +105,9 @@ class TalkTopicsViewModel(var pageTitle: PageTitle) : ViewModel() { val discussionToolsInfoResponse = ServiceFactory.get(pageTitle.wikiSite).getTalkPageTopics(pageTitle.prefixedText, OfflineCacheInterceptor.SAVE_HEADER_SAVE, pageTitle.wikiSite.languageCode, UriUtil.encodeURL(pageTitle.prefixedText)) + seenThreadItemsSha.clear() + seenThreadItemsSha.addAll(talkPageDao.getAll().map { it.sha }) + threadItems.clear() threadItems.addAll(discussionToolsInfoResponse.pageInfo?.threads ?: emptyList()) sortAndFilterThreadItems() @@ -112,6 +117,7 @@ class TalkTopicsViewModel(var pageTitle: PageTitle) : ViewModel() { isWatched = watchStatus.watched hasWatchlistExpiry = watchStatus.hasWatchlistExpiry() + uiState.value = UiState.LoadTopic(pageTitle, threadItems) } } @@ -134,15 +140,19 @@ class TalkTopicsViewModel(var pageTitle: PageTitle) : ViewModel() { viewModelScope.launch(actionHandler) { if (topicSeen(threadItem) && !force) { talkPageDao.deleteTalkPageSeen(it) + seenThreadItemsSha.remove(it) } else { talkPageDao.insertTalkPageSeen(TalkPageSeen(it)) + seenThreadItemsSha.add(it) } } } } - suspend fun topicSeen(threadItem: ThreadItem?): Boolean { - return threadSha(threadItem)?.run { talkPageDao.getTalkPageSeen(this) != null } ?: false + fun topicSeen(threadItem: ThreadItem?): Boolean { + return threadSha(threadItem)?.let { + seenThreadItemsSha.any { sha -> sha == it } + } ?: false } private fun threadSha(threadItem: ThreadItem?): String? { From 273c78f32d388409f07475fec1e987a8d70637f5 Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Tue, 1 Jul 2025 16:40:39 -0700 Subject: [PATCH 07/19] update function for talk page holder --- app/src/main/java/org/wikipedia/talk/TalkTopicHolder.kt | 5 +++-- app/src/main/java/org/wikipedia/talk/TalkTopicsViewModel.kt | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/wikipedia/talk/TalkTopicHolder.kt b/app/src/main/java/org/wikipedia/talk/TalkTopicHolder.kt index 2b46191dad1..9167d5bd9f3 100644 --- a/app/src/main/java/org/wikipedia/talk/TalkTopicHolder.kt +++ b/app/src/main/java/org/wikipedia/talk/TalkTopicHolder.kt @@ -124,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 fc0d144e9ef..048471b1eea 100644 --- a/app/src/main/java/org/wikipedia/talk/TalkTopicsViewModel.kt +++ b/app/src/main/java/org/wikipedia/talk/TalkTopicsViewModel.kt @@ -135,7 +135,7 @@ 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) { @@ -145,6 +145,7 @@ class TalkTopicsViewModel(var pageTitle: PageTitle) : ViewModel() { talkPageDao.insertTalkPageSeen(TalkPageSeen(it)) seenThreadItemsSha.add(it) } + action() } } } From 80fa4fe81aefad794e1ef1c46f8d53a90bffde01 Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Tue, 1 Jul 2025 17:11:46 -0700 Subject: [PATCH 08/19] Remove Transaction --- .../java/org/wikipedia/page/PageFragment.kt | 10 +++---- .../readinglist/ReadingListBehaviorsUtil.kt | 30 ++++++++----------- .../readinglist/db/ReadingListPageDao.kt | 13 -------- .../org/wikipedia/talk/TalkTopicsViewModel.kt | 1 - 4 files changed, 17 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/org/wikipedia/page/PageFragment.kt b/app/src/main/java/org/wikipedia/page/PageFragment.kt index 976818f9678..a4dad9f4530 100644 --- a/app/src/main/java/org/wikipedia/page/PageFragment.kt +++ b/app/src/main/java/org/wikipedia/page/PageFragment.kt @@ -1053,13 +1053,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/ReadingListBehaviorsUtil.kt b/app/src/main/java/org/wikipedia/readinglist/ReadingListBehaviorsUtil.kt index 8e30c3d686b..ff193dd904e 100644 --- a/app/src/main/java/org/wikipedia/readinglist/ReadingListBehaviorsUtil.kt +++ b/app/src/main/java/org/wikipedia/readinglist/ReadingListBehaviorsUtil.kt @@ -326,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 { 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 6b5443f061e..8b040d5e655 100644 --- a/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt +++ b/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt @@ -110,7 +110,6 @@ interface ReadingListPageDao { } } - @Transaction suspend fun addPagesToListIfNotExist(list: ReadingList, titles: List): List { val addedTitles = mutableListOf() for (title in titles) { @@ -151,7 +150,6 @@ interface ReadingListPageDao { }.toMutableList()) } - @Transaction suspend fun markPagesForDeletion(list: ReadingList, pages: List, queueForSync: Boolean = true) { for (page in pages) { page.status = ReadingListPage.STATUS_QUEUE_FOR_DELETE @@ -168,7 +166,6 @@ interface ReadingListPageDao { markPagesForOffline(listOf(page), offline, forcedSave) } - @Transaction suspend fun markPagesForOffline(pages: List, offline: Boolean, forcedSave: Boolean) { for (page in pages) { if (page.offline == offline && !forcedSave) { @@ -187,7 +184,6 @@ interface ReadingListPageDao { deletePagesByStatus(ReadingListPage.STATUS_QUEUE_FOR_DELETE) } - @Transaction suspend fun movePagesToListAndDeleteSourcePages(sourceList: ReadingList, destList: ReadingList, titles: List): List { val movedTitles = mutableListOf() for (title in titles) { @@ -223,15 +219,6 @@ interface ReadingListPageDao { ) } - suspend fun addPageToList(list: ReadingList, title: PageTitle, queueForSync: Boolean) { - addPageToList(list, title) - SavedPageSyncService.enqueue() - if (queueForSync) { - ReadingListSyncAdapter.manualSync() - } - } - - @Transaction suspend fun addPageToLists(lists: List, page: ReadingListPage, queueForSync: Boolean) { for (list in lists) { if (getPageByTitle(list, ReadingListPage.toPageTitle(page)) != null) { diff --git a/app/src/main/java/org/wikipedia/talk/TalkTopicsViewModel.kt b/app/src/main/java/org/wikipedia/talk/TalkTopicsViewModel.kt index 048471b1eea..e764ac07c31 100644 --- a/app/src/main/java/org/wikipedia/talk/TalkTopicsViewModel.kt +++ b/app/src/main/java/org/wikipedia/talk/TalkTopicsViewModel.kt @@ -117,7 +117,6 @@ class TalkTopicsViewModel(var pageTitle: PageTitle) : ViewModel() { isWatched = watchStatus.watched hasWatchlistExpiry = watchStatus.hasWatchlistExpiry() - uiState.value = UiState.LoadTopic(pageTitle, threadItems) } } From 586d455efccd857731f71f4f04696104cd64d7b3 Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Wed, 2 Jul 2025 16:10:10 -0700 Subject: [PATCH 09/19] Add transaction back --- .../readinglist/db/ReadingListDao.kt | 6 ++++++ .../readinglist/db/ReadingListPageDao.kt | 20 +++++++++++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) 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 88b2ab9e25b..333bff7ffb0 100644 --- a/app/src/main/java/org/wikipedia/readinglist/db/ReadingListDao.kt +++ b/app/src/main/java/org/wikipedia/readinglist/db/ReadingListDao.kt @@ -38,6 +38,7 @@ interface ReadingListDao { @Query("UPDATE ReadingList SET remoteId = -1") suspend fun markAllListsUnsynced() + @Transaction suspend fun getAllLists(): List { val lists = getListsWithoutContents() lists.forEach { @@ -46,6 +47,7 @@ interface ReadingListDao { return lists.toMutableList() } + @Transaction suspend fun getListById(id: Long, populatePages: Boolean): ReadingList? { return getListById(id)?.apply { if (populatePages) { @@ -54,6 +56,7 @@ interface ReadingListDao { } } + @Transaction suspend fun getAllListsWithUnsyncedPages(): List { val lists = getListsWithoutContents() val pages = AppDatabase.instance.readingListPageDao().getAllPagesToBeSynced() @@ -92,6 +95,7 @@ interface ReadingListDao { } } + @Transaction suspend fun getListsFromPageOccurrences(pages: List): List { val lists = getListsByIds(pages.map { it.listId }.toSet()) pages.forEach { page -> @@ -100,6 +104,7 @@ interface ReadingListDao { return lists } + @Transaction suspend fun createList(title: String, description: String?): ReadingList { if (title.isEmpty()) { L.w("Attempted to create list with empty title (default).") @@ -108,6 +113,7 @@ interface ReadingListDao { return createNewList(title, description) } + @Transaction suspend fun getDefaultList(): ReadingList { return getListsWithoutContents().find { it.isDefault } ?: run { L.w("(Re)creating default list.") 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 8b040d5e655..82c8cd71301 100644 --- a/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt +++ b/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt @@ -110,15 +110,18 @@ interface ReadingListPageDao { } } + @Transaction suspend fun addPagesToListIfNotExist(list: ReadingList, titles: List): List { val addedTitles = mutableListOf() + val protoPage = mutableListOf() for (title in titles) { if (getPageByTitle(list, title) != null) { continue } - addPageToList(list, title) + protoPage.add(addPageToList(list, title)) addedTitles.add(title.displayText) } + FlowEventBus.post(ArticleSavedOrDeletedEvent(true, *protoPage.toTypedArray())) if (addedTitles.isNotEmpty()) { SavedPageSyncService.enqueue() ReadingListSyncAdapter.manualSync() @@ -126,6 +129,7 @@ interface ReadingListPageDao { return addedTitles } + @Transaction suspend fun findPageInAnyList(title: PageTitle): ReadingListPage? { return getPageByParams( title.wikiSite, title.wikiSite.languageCode, title.namespace(), @@ -150,6 +154,7 @@ interface ReadingListPageDao { }.toMutableList()) } + @Transaction suspend fun markPagesForDeletion(list: ReadingList, pages: List, queueForSync: Boolean = true) { for (page in pages) { page.status = ReadingListPage.STATUS_QUEUE_FOR_DELETE @@ -166,6 +171,7 @@ interface ReadingListPageDao { markPagesForOffline(listOf(page), offline, forcedSave) } + @Transaction suspend fun markPagesForOffline(pages: List, offline: Boolean, forcedSave: Boolean) { for (page in pages) { if (page.offline == offline && !forcedSave) { @@ -197,6 +203,7 @@ interface ReadingListPageDao { return movedTitles } + @Transaction private suspend fun movePageToList(sourceList: ReadingList, destList: ReadingList, title: PageTitle) { if (sourceList.id == destList.id) { return @@ -204,7 +211,8 @@ interface ReadingListPageDao { 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() @@ -212,6 +220,7 @@ interface ReadingListPageDao { } } + @Transaction suspend fun getPageByTitle(list: ReadingList, title: PageTitle): ReadingListPage? { return getPageByParams( title.wikiSite, title.wikiSite.languageCode, title.namespace(), @@ -219,6 +228,7 @@ interface ReadingListPageDao { ) } + @Transaction suspend fun addPageToLists(lists: List, page: ReadingListPage, queueForSync: Boolean) { for (list in lists) { if (getPageByTitle(list, ReadingListPage.toPageTitle(page)) != null) { @@ -235,6 +245,7 @@ interface ReadingListPageDao { } } + @Transaction suspend fun getAllPageOccurrences(title: PageTitle): List { return getPagesByParams( title.wikiSite, title.wikiSite.languageCode, title.namespace(), @@ -242,10 +253,11 @@ interface ReadingListPageDao { ) } - private suspend fun addPageToList(list: ReadingList, title: PageTitle) { + @Transaction + private suspend fun addPageToList(list: ReadingList, title: PageTitle): ReadingListPage { val protoPage = ReadingListPage(title) insertPageIntoDb(list, protoPage) - FlowEventBus.post(ArticleSavedOrDeletedEvent(true, protoPage)) + return protoPage } private suspend fun insertPageIntoDb(list: ReadingList, page: ReadingListPage) { From 6b7ce39d7a1f7d6ed1d1de7f643bde9808c4819d Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Wed, 2 Jul 2025 16:13:33 -0700 Subject: [PATCH 10/19] Revert "Add transaction back" This reverts commit 586d455efccd857731f71f4f04696104cd64d7b3. --- .../readinglist/db/ReadingListDao.kt | 6 ------ .../readinglist/db/ReadingListPageDao.kt | 20 ++++--------------- 2 files changed, 4 insertions(+), 22 deletions(-) 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 333bff7ffb0..88b2ab9e25b 100644 --- a/app/src/main/java/org/wikipedia/readinglist/db/ReadingListDao.kt +++ b/app/src/main/java/org/wikipedia/readinglist/db/ReadingListDao.kt @@ -38,7 +38,6 @@ interface ReadingListDao { @Query("UPDATE ReadingList SET remoteId = -1") suspend fun markAllListsUnsynced() - @Transaction suspend fun getAllLists(): List { val lists = getListsWithoutContents() lists.forEach { @@ -47,7 +46,6 @@ interface ReadingListDao { return lists.toMutableList() } - @Transaction suspend fun getListById(id: Long, populatePages: Boolean): ReadingList? { return getListById(id)?.apply { if (populatePages) { @@ -56,7 +54,6 @@ interface ReadingListDao { } } - @Transaction suspend fun getAllListsWithUnsyncedPages(): List { val lists = getListsWithoutContents() val pages = AppDatabase.instance.readingListPageDao().getAllPagesToBeSynced() @@ -95,7 +92,6 @@ interface ReadingListDao { } } - @Transaction suspend fun getListsFromPageOccurrences(pages: List): List { val lists = getListsByIds(pages.map { it.listId }.toSet()) pages.forEach { page -> @@ -104,7 +100,6 @@ interface ReadingListDao { return lists } - @Transaction suspend fun createList(title: String, description: String?): ReadingList { if (title.isEmpty()) { L.w("Attempted to create list with empty title (default).") @@ -113,7 +108,6 @@ interface ReadingListDao { return createNewList(title, description) } - @Transaction suspend fun getDefaultList(): ReadingList { return getListsWithoutContents().find { it.isDefault } ?: run { L.w("(Re)creating default list.") 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 82c8cd71301..8b040d5e655 100644 --- a/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt +++ b/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt @@ -110,18 +110,15 @@ interface ReadingListPageDao { } } - @Transaction suspend fun addPagesToListIfNotExist(list: ReadingList, titles: List): List { val addedTitles = mutableListOf() - val protoPage = mutableListOf() for (title in titles) { if (getPageByTitle(list, title) != null) { continue } - protoPage.add(addPageToList(list, title)) + addPageToList(list, title) addedTitles.add(title.displayText) } - FlowEventBus.post(ArticleSavedOrDeletedEvent(true, *protoPage.toTypedArray())) if (addedTitles.isNotEmpty()) { SavedPageSyncService.enqueue() ReadingListSyncAdapter.manualSync() @@ -129,7 +126,6 @@ interface ReadingListPageDao { return addedTitles } - @Transaction suspend fun findPageInAnyList(title: PageTitle): ReadingListPage? { return getPageByParams( title.wikiSite, title.wikiSite.languageCode, title.namespace(), @@ -154,7 +150,6 @@ interface ReadingListPageDao { }.toMutableList()) } - @Transaction suspend fun markPagesForDeletion(list: ReadingList, pages: List, queueForSync: Boolean = true) { for (page in pages) { page.status = ReadingListPage.STATUS_QUEUE_FOR_DELETE @@ -171,7 +166,6 @@ interface ReadingListPageDao { markPagesForOffline(listOf(page), offline, forcedSave) } - @Transaction suspend fun markPagesForOffline(pages: List, offline: Boolean, forcedSave: Boolean) { for (page in pages) { if (page.offline == offline && !forcedSave) { @@ -203,7 +197,6 @@ interface ReadingListPageDao { return movedTitles } - @Transaction private suspend fun movePageToList(sourceList: ReadingList, destList: ReadingList, title: PageTitle) { if (sourceList.id == destList.id) { return @@ -211,8 +204,7 @@ interface ReadingListPageDao { val sourceReadingListPage = getPageByTitle(sourceList, title) if (sourceReadingListPage != null) { if (getPageByTitle(destList, title) == null) { - val page = addPageToList(destList, title) - FlowEventBus.post(ArticleSavedOrDeletedEvent(true, page)) + addPageToList(destList, title) } markPagesForDeletion(sourceList, listOf(sourceReadingListPage)) ReadingListSyncAdapter.manualSync() @@ -220,7 +212,6 @@ interface ReadingListPageDao { } } - @Transaction suspend fun getPageByTitle(list: ReadingList, title: PageTitle): ReadingListPage? { return getPageByParams( title.wikiSite, title.wikiSite.languageCode, title.namespace(), @@ -228,7 +219,6 @@ interface ReadingListPageDao { ) } - @Transaction suspend fun addPageToLists(lists: List, page: ReadingListPage, queueForSync: Boolean) { for (list in lists) { if (getPageByTitle(list, ReadingListPage.toPageTitle(page)) != null) { @@ -245,7 +235,6 @@ interface ReadingListPageDao { } } - @Transaction suspend fun getAllPageOccurrences(title: PageTitle): List { return getPagesByParams( title.wikiSite, title.wikiSite.languageCode, title.namespace(), @@ -253,11 +242,10 @@ interface ReadingListPageDao { ) } - @Transaction - private suspend fun addPageToList(list: ReadingList, title: PageTitle): ReadingListPage { + private suspend fun addPageToList(list: ReadingList, title: PageTitle) { val protoPage = ReadingListPage(title) insertPageIntoDb(list, protoPage) - return protoPage + FlowEventBus.post(ArticleSavedOrDeletedEvent(true, protoPage)) } private suspend fun insertPageIntoDb(list: ReadingList, page: ReadingListPage) { From c4cdb3cde3b95ea5b5e34c15f63b1ad6d172a975 Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Wed, 2 Jul 2025 16:34:59 -0700 Subject: [PATCH 11/19] Update the logic of posting the flowBus --- .../wikipedia/readinglist/db/ReadingListPageDao.kt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) 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 8b040d5e655..37a18853114 100644 --- a/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt +++ b/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt @@ -112,13 +112,16 @@ interface ReadingListPageDao { suspend fun addPagesToListIfNotExist(list: ReadingList, titles: List): List { val addedTitles = mutableListOf() + val pages = mutableListOf() for (title in titles) { if (getPageByTitle(list, title) != null) { continue } - addPageToList(list, title) + val page = addPageToList(list, title) + pages.add(page) addedTitles.add(title.displayText) } + FlowEventBus.post(ArticleSavedOrDeletedEvent(true, *pages.toTypedArray())) if (addedTitles.isNotEmpty()) { SavedPageSyncService.enqueue() ReadingListSyncAdapter.manualSync() @@ -204,7 +207,8 @@ interface ReadingListPageDao { 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() @@ -242,10 +246,10 @@ interface ReadingListPageDao { ) } - private suspend 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 suspend fun insertPageIntoDb(list: ReadingList, page: ReadingListPage) { From dd132c6c66f6cc90453b1d4c1e09c8ec7bf318bd Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Mon, 7 Jul 2025 11:26:31 -0700 Subject: [PATCH 12/19] Adding suspend to Notification functions --- .../main/java/org/wikipedia/WikipediaApp.kt | 24 ++++++----- .../NotificationPollBroadcastReceiver.kt | 41 +++++++++++-------- .../notifications/NotificationRepository.kt | 8 +--- .../notifications/NotificationViewModel.kt | 2 +- .../notifications/db/NotificationDao.kt | 10 ++--- 5 files changed, 46 insertions(+), 39 deletions(-) diff --git a/app/src/main/java/org/wikipedia/WikipediaApp.kt b/app/src/main/java/org/wikipedia/WikipediaApp.kt index 61f13707879..ed81b14248c 100644 --- a/app/src/main/java/org/wikipedia/WikipediaApp.kt +++ b/app/src/main/java/org/wikipedia/WikipediaApp.kt @@ -251,16 +251,20 @@ class WikipediaApp : Application() { } fun resetAfterLogOut() { - AccountUtil.removeAccount() - Prefs.isPushNotificationTokenSubscribed = false - Prefs.pushNotificationTokenOld = "" - Prefs.tempAccountWelcomeShown = false - Prefs.tempAccountCreateDay = 0L - Prefs.tempAccountDialogShown = false - SharedPreferenceCookieManager.instance.clearAllCookies() - AppDatabase.instance.notificationDao().deleteAll() - FlowEventBus.post(LoggedOutEvent()) - L.d("Logout complete.") + MainScope().launch(CoroutineExceptionHandler { _, t -> + L.e(t) + }) { + AccountUtil.removeAccount() + Prefs.isPushNotificationTokenSubscribed = false + Prefs.pushNotificationTokenOld = "" + Prefs.tempAccountWelcomeShown = false + Prefs.tempAccountCreateDay = 0L + Prefs.tempAccountDialogShown = false + SharedPreferenceCookieManager.instance.clearAllCookies() + AppDatabase.instance.notificationDao().deleteAll() + FlowEventBus.post(LoggedOutEvent()) + L.d("Logout complete.") + } } private fun enableWebViewDebugging() { 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..07285fda758 100644 --- a/app/src/main/java/org/wikipedia/notifications/db/NotificationDao.kt +++ b/app/src/main/java/org/wikipedia/notifications/db/NotificationDao.kt @@ -11,7 +11,7 @@ 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 +20,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): Flow> @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? } From bf2f2594d07cb100538605c79816ba1a53c6b62c Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Mon, 7 Jul 2025 11:32:45 -0700 Subject: [PATCH 13/19] Fix test --- .../wikipedia/database/AppDatabaseTests.kt | 29 +++++++++++-------- .../notifications/db/NotificationDao.kt | 3 +- 2 files changed, 18 insertions(+), 14 deletions(-) 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/main/java/org/wikipedia/notifications/db/NotificationDao.kt b/app/src/main/java/org/wikipedia/notifications/db/NotificationDao.kt index 07285fda758..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,7 +6,6 @@ import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Update -import kotlinx.coroutines.flow.Flow @Dao interface NotificationDao { @@ -26,7 +25,7 @@ interface NotificationDao { suspend fun getAllNotifications(): List @Query("SELECT * FROM Notification WHERE `wiki` IN (:wiki)") - suspend fun getNotificationsByWiki(wiki: List): Flow> + suspend fun getNotificationsByWiki(wiki: List): List @Query("SELECT * FROM Notification WHERE `wiki` IN (:wiki) AND `id` IN (:id)") suspend fun getNotificationById(wiki: String, id: Long): Notification? From bb4666af6c10cccbf948aec300410f464e893e49 Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Mon, 7 Jul 2025 13:44:14 -0700 Subject: [PATCH 14/19] remove duplicated function --- .../readinglist/ReadingListBehaviorsUtil.kt | 2 +- .../readinglist/db/ReadingListPageDao.kt | 22 +++++++++---------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/wikipedia/readinglist/ReadingListBehaviorsUtil.kt b/app/src/main/java/org/wikipedia/readinglist/ReadingListBehaviorsUtil.kt index ff193dd904e..ffbf0e2646d 100644 --- a/app/src/main/java/org/wikipedia/readinglist/ReadingListBehaviorsUtil.kt +++ b/app/src/main/java/org/wikipedia/readinglist/ReadingListBehaviorsUtil.kt @@ -357,7 +357,7 @@ object ReadingListBehaviorsUtil { activity.lifecycleScope.launch(exceptionHandler) { AppDatabase.instance.readingListPageDao() - .markPageForOffline(page, !page.offline, forcedSave) + .markPagesForOffline(listOf(page), !page.offline, forcedSave) FeedbackUtil.showMessage( activity, activity.resources.getQuantityString( 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 37a18853114..10820883c11 100644 --- a/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt +++ b/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt @@ -153,6 +153,7 @@ interface ReadingListPageDao { }.toMutableList()) } + @Transaction suspend fun markPagesForDeletion(list: ReadingList, pages: List, queueForSync: Boolean = true) { for (page in pages) { page.status = ReadingListPage.STATUS_QUEUE_FOR_DELETE @@ -165,21 +166,18 @@ interface ReadingListPageDao { SavedPageSyncService.enqueue() } - suspend fun markPageForOffline(page: ReadingListPage, offline: Boolean, forcedSave: Boolean) { - markPagesForOffline(listOf(page), offline, forcedSave) - } - + @Transaction suspend fun markPagesForOffline(pages: List, offline: Boolean, forcedSave: Boolean) { - for (page in pages) { - if (page.offline == offline && !forcedSave) { - continue + val updatedPages = pages.map { + if (it.offline != offline || !forcedSave) { + it.offline = offline + if (forcedSave) { + it.status = ReadingListPage.STATUS_QUEUE_FOR_FORCED_SAVE + } } - page.offline = offline - if (forcedSave) { - page.status = ReadingListPage.STATUS_QUEUE_FOR_FORCED_SAVE - } - updateReadingListPage(page) + it } + updateReadingListPages(updatedPages) SavedPageSyncService.enqueue() } From eb73431f9acdc41df6961ce524a84454127d9bf9 Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Mon, 7 Jul 2025 15:28:29 -0700 Subject: [PATCH 15/19] Simplify --- .../org/wikipedia/readinglist/db/ReadingListPageDao.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) 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 10820883c11..af146de0093 100644 --- a/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt +++ b/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt @@ -111,7 +111,6 @@ interface ReadingListPageDao { } suspend fun addPagesToListIfNotExist(list: ReadingList, titles: List): List { - val addedTitles = mutableListOf() val pages = mutableListOf() for (title in titles) { if (getPageByTitle(list, title) != null) { @@ -119,14 +118,13 @@ interface ReadingListPageDao { } val page = addPageToList(list, title) pages.add(page) - addedTitles.add(title.displayText) } - FlowEventBus.post(ArticleSavedOrDeletedEvent(true, *pages.toTypedArray())) - if (addedTitles.isNotEmpty()) { + if (pages.isNotEmpty()) { SavedPageSyncService.enqueue() ReadingListSyncAdapter.manualSync() + FlowEventBus.post(ArticleSavedOrDeletedEvent(true, *pages.toTypedArray())) } - return addedTitles + return pages.map { it.displayTitle } } suspend fun findPageInAnyList(title: PageTitle): ReadingListPage? { From 79aeba435735c0f2983b7553ab911d8a65fc031d Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Mon, 7 Jul 2025 15:34:49 -0700 Subject: [PATCH 16/19] Update transation for moving lists --- .../java/org/wikipedia/readinglist/db/ReadingListPageDao.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 af146de0093..f3a15661222 100644 --- a/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt +++ b/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt @@ -110,6 +110,7 @@ interface ReadingListPageDao { } } + @Transaction suspend fun addPagesToListIfNotExist(list: ReadingList, titles: List): List { val pages = mutableListOf() for (title in titles) { @@ -127,6 +128,7 @@ interface ReadingListPageDao { return pages.map { it.displayTitle } } + @Transaction suspend fun findPageInAnyList(title: PageTitle): ReadingListPage? { return getPageByParams( title.wikiSite, title.wikiSite.languageCode, title.namespace(), @@ -183,6 +185,7 @@ interface ReadingListPageDao { deletePagesByStatus(ReadingListPage.STATUS_QUEUE_FOR_DELETE) } + @Transaction suspend fun movePagesToListAndDeleteSourcePages(sourceList: ReadingList, destList: ReadingList, titles: List): List { val movedTitles = mutableListOf() for (title in titles) { @@ -196,7 +199,8 @@ interface ReadingListPageDao { return movedTitles } - private suspend fun movePageToList(sourceList: ReadingList, destList: ReadingList, title: PageTitle) { + @Transaction + suspend fun movePageToList(sourceList: ReadingList, destList: ReadingList, title: PageTitle) { if (sourceList.id == destList.id) { return } From 2f841e58128c2ac4eba5e40f68943831bada2284 Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Wed, 16 Jul 2025 11:10:26 -0700 Subject: [PATCH 17/19] Keep same reset and logout and add coroutine for notificationDao only --- .../main/java/org/wikipedia/WikipediaApp.kt | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/org/wikipedia/WikipediaApp.kt b/app/src/main/java/org/wikipedia/WikipediaApp.kt index ed81b14248c..1f70072bc6b 100644 --- a/app/src/main/java/org/wikipedia/WikipediaApp.kt +++ b/app/src/main/java/org/wikipedia/WikipediaApp.kt @@ -251,20 +251,18 @@ class WikipediaApp : Application() { } fun resetAfterLogOut() { - MainScope().launch(CoroutineExceptionHandler { _, t -> - L.e(t) - }) { - AccountUtil.removeAccount() - Prefs.isPushNotificationTokenSubscribed = false - Prefs.pushNotificationTokenOld = "" - Prefs.tempAccountWelcomeShown = false - Prefs.tempAccountCreateDay = 0L - Prefs.tempAccountDialogShown = false - SharedPreferenceCookieManager.instance.clearAllCookies() + AccountUtil.removeAccount() + Prefs.isPushNotificationTokenSubscribed = false + Prefs.pushNotificationTokenOld = "" + Prefs.tempAccountWelcomeShown = false + Prefs.tempAccountCreateDay = 0L + Prefs.tempAccountDialogShown = false + SharedPreferenceCookieManager.instance.clearAllCookies() + MainScope().launch { AppDatabase.instance.notificationDao().deleteAll() - FlowEventBus.post(LoggedOutEvent()) - L.d("Logout complete.") } + FlowEventBus.post(LoggedOutEvent()) + L.d("Logout complete.") } private fun enableWebViewDebugging() { From 37fd707270c4f00192a46fe6ca521169e0d407db Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Tue, 19 Aug 2025 14:04:14 -0400 Subject: [PATCH 18/19] Follow-up to updating DAO: improve efficiency. (#5854) --- .../main/java/org/wikipedia/talk/TalkTopicsViewModel.kt | 7 ++++--- app/src/main/java/org/wikipedia/talk/db/TalkPageSeenDao.kt | 3 +++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/wikipedia/talk/TalkTopicsViewModel.kt b/app/src/main/java/org/wikipedia/talk/TalkTopicsViewModel.kt index e764ac07c31..2f5dc785d6a 100644 --- a/app/src/main/java/org/wikipedia/talk/TalkTopicsViewModel.kt +++ b/app/src/main/java/org/wikipedia/talk/TalkTopicsViewModel.kt @@ -105,13 +105,14 @@ class TalkTopicsViewModel(var pageTitle: PageTitle) : ViewModel() { val discussionToolsInfoResponse = ServiceFactory.get(pageTitle.wikiSite).getTalkPageTopics(pageTitle.prefixedText, OfflineCacheInterceptor.SAVE_HEADER_SAVE, pageTitle.wikiSite.languageCode, UriUtil.encodeURL(pageTitle.prefixedText)) - seenThreadItemsSha.clear() - seenThreadItemsSha.addAll(talkPageDao.getAll().map { it.sha }) - threadItems.clear() 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 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 6534c3cd2bf..b452560d8bb 100644 --- a/app/src/main/java/org/wikipedia/talk/db/TalkPageSeenDao.kt +++ b/app/src/main/java/org/wikipedia/talk/db/TalkPageSeenDao.kt @@ -16,6 +16,9 @@ interface TalkPageSeenDao { @Query("SELECT * FROM TalkPageSeen") 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) From 5bdb30335f2ec99c3f547cceadfa69d18a064ad9 Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Tue, 19 Aug 2025 11:31:00 -0700 Subject: [PATCH 19/19] Use MainScope instead of AppCompatActivity --- .../readinglist/ReadingListBehaviorsUtil.kt | 56 +++++++++---------- .../readinglist/ReadingListFragment.kt | 32 +++++------ .../readinglist/ReadingListsFragment.kt | 22 ++++---- .../RemoveFromReadingListsDialog.kt | 12 ++-- 4 files changed, 61 insertions(+), 61 deletions(-) diff --git a/app/src/main/java/org/wikipedia/readinglist/ReadingListBehaviorsUtil.kt b/app/src/main/java/org/wikipedia/readinglist/ReadingListBehaviorsUtil.kt index ffbf0e2646d..b4a8b79c473 100644 --- a/app/src/main/java/org/wikipedia/readinglist/ReadingListBehaviorsUtil.kt +++ b/app/src/main/java/org/wikipedia/readinglist/ReadingListBehaviorsUtil.kt @@ -51,7 +51,7 @@ object ReadingListBehaviorsUtil { fun getListsContainPage(readingListPage: ReadingListPage) = allReadingLists.filter { list -> list.pages.any { it.apiTitle == readingListPage.apiTitle } } - fun savePagesForOffline(activity: AppCompatActivity, selectedPages: List, callback: Callback) { + fun savePagesForOffline(activity: Activity, selectedPages: List, callback: Callback) { if (Prefs.isDownloadOnlyOverWiFiEnabled && !DeviceUtil.isOnWiFi) { showMobileDataWarningDialog(activity) { _, _ -> savePagesForOffline(activity, selectedPages, true) @@ -63,9 +63,9 @@ object ReadingListBehaviorsUtil { } } - private fun savePagesForOffline(activity: AppCompatActivity, selectedPages: List, forcedSave: Boolean) { + private fun savePagesForOffline(activity: Activity, selectedPages: List, forcedSave: Boolean) { if (selectedPages.isNotEmpty()) { - activity.lifecycleScope.launch(exceptionHandler) { + MainScope().launch(exceptionHandler) { for (page in selectedPages) { resetPageProgress(page) } @@ -76,9 +76,9 @@ object ReadingListBehaviorsUtil { } } - fun removePagesFromOffline(activity: AppCompatActivity, selectedPages: List, callback: Callback) { + fun removePagesFromOffline(activity: Activity, selectedPages: List, callback: Callback) { if (selectedPages.isNotEmpty()) { - activity.lifecycleScope.launch(exceptionHandler) { + MainScope().launch(exceptionHandler) { AppDatabase.instance.readingListPageDao().markPagesForOffline(selectedPages, offline = false, forcedSave = false) showMultiSelectOfflineStateChangeSnackbar(activity, selectedPages, false) callback.onCompleted() @@ -86,7 +86,7 @@ object ReadingListBehaviorsUtil { } } - fun deleteReadingList(activity: AppCompatActivity, readingList: ReadingList?, showDialog: Boolean, callback: Callback) { + fun deleteReadingList(activity: Activity, readingList: ReadingList?, showDialog: Boolean, callback: Callback) { if (readingList == null) { return } @@ -94,7 +94,7 @@ 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) { _, _ -> - activity.lifecycleScope.launch(exceptionHandler) { + MainScope().launch(exceptionHandler) { AppDatabase.instance.readingListDao().deleteList(readingList) AppDatabase.instance.readingListPageDao() .markPagesForDeletion(readingList, readingList.pages, false) @@ -104,7 +104,7 @@ object ReadingListBehaviorsUtil { .setNegativeButton(R.string.reading_list_delete_dialog_cancel_button_text, null) .show() } else { - activity.lifecycleScope.launch(exceptionHandler) { + MainScope().launch(exceptionHandler) { AppDatabase.instance.readingListDao().deleteList(readingList) AppDatabase.instance.readingListPageDao() .markPagesForDeletion(readingList, readingList.pages, false) @@ -113,12 +113,12 @@ object ReadingListBehaviorsUtil { } } - fun deleteReadingLists(activity: AppCompatActivity, readingLists: List, callback: Callback) { + fun deleteReadingLists(activity: Activity, readingLists: List, callback: Callback) { MaterialAlertDialogBuilder(activity) .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) { _, _ -> - activity.lifecycleScope.launch(exceptionHandler) { + MainScope().launch(exceptionHandler) { readingLists.filterNot { it.isDefault }.forEach { AppDatabase.instance.readingListDao().deleteList(it) AppDatabase.instance.readingListPageDao().markPagesForDeletion(it, it.pages, false) @@ -130,8 +130,8 @@ object ReadingListBehaviorsUtil { .show() } - fun deletePages(activity: AppCompatActivity, listsContainPage: List, readingListPage: ReadingListPage, snackbarCallback: SnackbarCallback, callback: Callback) { - activity.lifecycleScope.launch(exceptionHandler) { + 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)) @@ -159,7 +159,7 @@ object ReadingListBehaviorsUtil { } } - fun renameReadingList(activity: AppCompatActivity, readingList: ReadingList?, callback: Callback) { + fun renameReadingList(activity: Activity, readingList: ReadingList?, callback: Callback) { if (readingList == null) { return } else if (readingList.isDefault) { @@ -167,7 +167,7 @@ object ReadingListBehaviorsUtil { return } - activity.lifecycleScope.launch(exceptionHandler) { + MainScope().launch(exceptionHandler) { val existingTitles = AppDatabase.instance.readingListDao().getListsWithoutContents() .map { it.title } .filterNot { it == readingList.title } @@ -176,7 +176,7 @@ object ReadingListBehaviorsUtil { activity, readingList.title, readingList.description, existingTitles, callback = object : ReadingListTitleDialog.Callback { override fun onSuccess(text: String, description: String) { - activity.lifecycleScope.launch(exceptionHandler) { + MainScope().launch(exceptionHandler) { readingList.title = text readingList.description = description readingList.dirty = true @@ -188,7 +188,7 @@ object ReadingListBehaviorsUtil { } } - private fun showDeletePageFromListsUndoSnackbar(activity: AppCompatActivity, lists: List?, page: ReadingListPage, callback: SnackbarCallback) { + private fun showDeletePageFromListsUndoSnackbar(activity: Activity, lists: List?, page: ReadingListPage, callback: SnackbarCallback) { if (lists == null) { return } @@ -202,7 +202,7 @@ 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) { - activity.lifecycleScope.launch(exceptionHandler) { + MainScope().launch(exceptionHandler) { AppDatabase.instance.readingListPageDao().addPageToLists(lists, page, true) callback.onUndoDeleteClicked() } @@ -210,7 +210,7 @@ object ReadingListBehaviorsUtil { .show() } - fun showDeletePagesUndoSnackbar(activity: AppCompatActivity, readingList: ReadingList?, pages: List, callback: SnackbarCallback) { + fun showDeletePagesUndoSnackbar(activity: Activity, readingList: ReadingList?, pages: List, callback: SnackbarCallback) { if (readingList == null) { return } @@ -219,7 +219,7 @@ 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) { - activity.lifecycleScope.launch(exceptionHandler) { + MainScope().launch(exceptionHandler) { val newPages = pages.map { ReadingListPage(ReadingListPage.toPageTitle(it)) } AppDatabase.instance.readingListPageDao().addPagesToList(readingList, newPages, true) readingList.pages.addAll(newPages) @@ -229,13 +229,13 @@ object ReadingListBehaviorsUtil { .show() } - fun showDeleteListUndoSnackbar(activity: AppCompatActivity, readingList: ReadingList?, callback: SnackbarCallback) { + fun showDeleteListUndoSnackbar(activity: Activity, readingList: ReadingList?, callback: SnackbarCallback) { if (readingList == null) { return } FeedbackUtil.makeSnackbar(activity, activity.getString(R.string.reading_list_deleted, readingList.title)) .setAction(R.string.reading_list_item_delete_undo) { - activity.lifecycleScope.launch(exceptionHandler) { + MainScope().launch(exceptionHandler) { val newList = AppDatabase.instance.readingListDao() .createList(readingList.title, readingList.description) val newPages = readingList.pages.map { ReadingListPage(ReadingListPage.toPageTitle(it)) } @@ -247,14 +247,14 @@ object ReadingListBehaviorsUtil { .show() } - fun showDeleteListsUndoSnackbar(activity: AppCompatActivity, readingLists: List?, callback: SnackbarCallback) { + fun showDeleteListsUndoSnackbar(activity: Activity, readingLists: List?, callback: SnackbarCallback) { if (readingLists == null) { return } val snackBar = FeedbackUtil.makeSnackbar(activity, getDeleteListMessage(activity, readingLists)) if (!(readingLists.size == 1 && readingLists[0].isDefault)) { snackBar.setAction(R.string.reading_list_item_delete_undo) { - activity.lifecycleScope.launch(exceptionHandler) { + MainScope().launch(exceptionHandler) { readingLists.filterNot { it.isDefault }.forEach { val newList = AppDatabase.instance.readingListDao() .createList(it.title, it.description) @@ -283,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) @@ -309,7 +309,7 @@ object ReadingListBehaviorsUtil { } } - fun toggleOffline(activity: AppCompatActivity, page: ReadingListPage, callback: Callback) { + fun toggleOffline(activity: Activity, page: ReadingListPage, callback: Callback) { resetPageProgress(page) if (Prefs.isDownloadOnlyOverWiFiEnabled && !DeviceUtil.isOnWiFi && !page.offline) { showMobileDataWarningDialog(activity) { _, _ -> @@ -353,9 +353,9 @@ object ReadingListBehaviorsUtil { MoveToReadingListDialog.newInstance(sourceReadingListId, title, source, showDefaultList, listener)) } - private fun toggleOffline(activity: AppCompatActivity, page: ReadingListPage, forcedSave: Boolean) { + private fun toggleOffline(activity: Activity, page: ReadingListPage, forcedSave: Boolean) { - activity.lifecycleScope.launch(exceptionHandler) { + MainScope().launch(exceptionHandler) { AppDatabase.instance.readingListPageDao() .markPagesForOffline(listOf(page), !page.offline, forcedSave) FeedbackUtil.showMessage( diff --git a/app/src/main/java/org/wikipedia/readinglist/ReadingListFragment.kt b/app/src/main/java/org/wikipedia/readinglist/ReadingListFragment.kt index a726e11067a..4a469260d59 100644 --- a/app/src/main/java/org/wikipedia/readinglist/ReadingListFragment.kt +++ b/app/src/main/java/org/wikipedia/readinglist/ReadingListFragment.kt @@ -205,7 +205,7 @@ class ReadingListFragment : Fragment(), MenuProvider, ReadingListItemActionsDial readingList?.let { val pages = resource.data it.pages.removeAll(pages) - ReadingListBehaviorsUtil.showDeletePagesUndoSnackbar(requireActivity() as AppCompatActivity, it, pages) { updateReadingListData() } + ReadingListBehaviorsUtil.showDeletePagesUndoSnackbar(requireActivity(), it, pages) { updateReadingListData() } update() } } @@ -351,7 +351,7 @@ class ReadingListFragment : Fragment(), MenuProvider, ReadingListItemActionsDial } R.id.menu_reading_list_save_all_offline -> { readingList?.let { - ReadingListBehaviorsUtil.savePagesForOffline(requireActivity() as AppCompatActivity, it.pages) { + ReadingListBehaviorsUtil.savePagesForOffline(requireActivity(), it.pages) { adapter.notifyDataSetChanged() update() } @@ -360,7 +360,7 @@ class ReadingListFragment : Fragment(), MenuProvider, ReadingListItemActionsDial } R.id.menu_reading_list_remove_all_offline -> { readingList?.let { - ReadingListBehaviorsUtil.removePagesFromOffline(requireActivity() as AppCompatActivity, it.pages) { + ReadingListBehaviorsUtil.removePagesFromOffline(requireActivity(), it.pages) { adapter.notifyDataSetChanged() update() } @@ -677,7 +677,7 @@ class ReadingListFragment : Fragment(), MenuProvider, ReadingListItemActionsDial private fun delete() { readingList?.let { - ReadingListBehaviorsUtil.deleteReadingList(requireActivity() as AppCompatActivity, it, true) { + ReadingListBehaviorsUtil.deleteReadingList(requireActivity(), it, true) { startActivity(MainActivity.newIntent(requireActivity()).putExtra(Constants.INTENT_EXTRA_DELETE_READING_LIST, it.title)) requireActivity().finish() } @@ -686,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() } @@ -721,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() }) } @@ -857,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() }) } @@ -942,14 +942,14 @@ class ReadingListFragment : Fragment(), MenuProvider, ReadingListItemActionsDial } override fun onSaveAllOffline(readingList: ReadingList) { - ReadingListBehaviorsUtil.savePagesForOffline(requireActivity() as AppCompatActivity, readingList.pages) { + ReadingListBehaviorsUtil.savePagesForOffline(requireActivity(), readingList.pages) { adapter.notifyDataSetChanged() update() } } override fun onRemoveAllOffline(readingList: ReadingList) { - ReadingListBehaviorsUtil.removePagesFromOffline(requireActivity() as AppCompatActivity, readingList.pages) { + ReadingListBehaviorsUtil.removePagesFromOffline(requireActivity(), readingList.pages) { adapter.notifyDataSetChanged() update() } @@ -1004,18 +1004,18 @@ class ReadingListFragment : Fragment(), MenuProvider, ReadingListItemActionsDial } override fun onDelete(readingList: ReadingList) { - ReadingListBehaviorsUtil.deleteReadingList(requireActivity() as AppCompatActivity, readingList, true) { - ReadingListBehaviorsUtil.showDeleteListUndoSnackbar(requireActivity() as AppCompatActivity, readingList) { setSearchQuery() } + ReadingListBehaviorsUtil.deleteReadingList(requireActivity(), readingList, true) { + ReadingListBehaviorsUtil.showDeleteListUndoSnackbar(requireActivity(), readingList) { setSearchQuery() } setSearchQuery() } } override fun onSaveAllOffline(readingList: ReadingList) { - ReadingListBehaviorsUtil.savePagesForOffline(requireActivity() as AppCompatActivity, readingList.pages) { setSearchQuery() } + ReadingListBehaviorsUtil.savePagesForOffline(requireActivity(), readingList.pages) { setSearchQuery() } } override fun onRemoveAllOffline(readingList: ReadingList) { - ReadingListBehaviorsUtil.removePagesFromOffline(requireActivity() as AppCompatActivity, readingList.pages) { setSearchQuery() } + ReadingListBehaviorsUtil.removePagesFromOffline(requireActivity(), readingList.pages) { setSearchQuery() } } override fun onShare(readingList: ReadingList) { @@ -1060,7 +1060,7 @@ class ReadingListFragment : Fragment(), MenuProvider, ReadingListItemActionsDial if (it.saving) { Toast.makeText(context, R.string.reading_list_article_save_in_progress, Toast.LENGTH_LONG).show() } else { - ReadingListBehaviorsUtil.toggleOffline(requireActivity() as AppCompatActivity, item) { + ReadingListBehaviorsUtil.toggleOffline(requireActivity(), item) { adapter.notifyDataSetChanged() update() } @@ -1125,7 +1125,7 @@ class ReadingListFragment : Fragment(), MenuProvider, ReadingListItemActionsDial return true } R.id.menu_remove_from_offline -> { - ReadingListBehaviorsUtil.removePagesFromOffline(requireActivity() as AppCompatActivity, selectedPages) { + ReadingListBehaviorsUtil.removePagesFromOffline(requireActivity(), selectedPages) { adapter.notifyDataSetChanged() update() } @@ -1133,7 +1133,7 @@ class ReadingListFragment : Fragment(), MenuProvider, ReadingListItemActionsDial return true } R.id.menu_save_for_offline -> { - ReadingListBehaviorsUtil.savePagesForOffline(requireActivity() as AppCompatActivity, selectedPages) { + ReadingListBehaviorsUtil.savePagesForOffline(requireActivity(), selectedPages) { adapter.notifyDataSetChanged() update() } diff --git a/app/src/main/java/org/wikipedia/readinglist/ReadingListsFragment.kt b/app/src/main/java/org/wikipedia/readinglist/ReadingListsFragment.kt index 5f9d5c53c0b..ab31b9879f5 100644 --- a/app/src/main/java/org/wikipedia/readinglist/ReadingListsFragment.kt +++ b/app/src/main/java/org/wikipedia/readinglist/ReadingListsFragment.kt @@ -187,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) { @@ -213,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? { @@ -522,18 +522,18 @@ class ReadingListsFragment : Fragment(), SortReadingListsDialog.Callback, Readin } override fun onDelete(readingList: ReadingList) { - ReadingListBehaviorsUtil.deleteReadingList(requireActivity() as AppCompatActivity, readingList, true) { - ReadingListBehaviorsUtil.showDeleteListUndoSnackbar(requireActivity() as AppCompatActivity, readingList) { updateLists() } + ReadingListBehaviorsUtil.deleteReadingList(requireActivity(), readingList, true) { + ReadingListBehaviorsUtil.showDeleteListUndoSnackbar(requireActivity(), readingList) { updateLists() } updateLists() } } override fun onSaveAllOffline(readingList: ReadingList) { - ReadingListBehaviorsUtil.savePagesForOffline(requireActivity() as AppCompatActivity, readingList.pages) { updateLists(currentSearchQuery, true) } + ReadingListBehaviorsUtil.savePagesForOffline(requireActivity(), readingList.pages) { updateLists(currentSearchQuery, true) } } override fun onRemoveAllOffline(readingList: ReadingList) { - ReadingListBehaviorsUtil.removePagesFromOffline(requireActivity() as AppCompatActivity, readingList.pages) { updateLists(currentSearchQuery, true) } + ReadingListBehaviorsUtil.removePagesFromOffline(requireActivity(), readingList.pages) { updateLists(currentSearchQuery, true) } } override fun onSelectList(readingList: ReadingList) { @@ -604,7 +604,7 @@ class ReadingListsFragment : Fragment(), SortReadingListsDialog.Callback, Readin if (it.saving) { Toast.makeText(context, R.string.reading_list_article_save_in_progress, Toast.LENGTH_LONG).show() } else { - ReadingListBehaviorsUtil.toggleOffline(requireActivity() as AppCompatActivity, it) { adapter.notifyDataSetChanged() } + ReadingListBehaviorsUtil.toggleOffline(requireActivity(), it) { adapter.notifyDataSetChanged() } } } } @@ -621,8 +621,8 @@ class ReadingListsFragment : Fragment(), SortReadingListsDialog.Callback, Readin requireActivity().intent.removeExtra(Constants.INTENT_EXTRA_DELETE_READING_LIST) displayedLists.forEach { if (it is ReadingList && it.title == titleToDelete) { - ReadingListBehaviorsUtil.deleteReadingList(requireActivity() as AppCompatActivity, it, false) { - ReadingListBehaviorsUtil.showDeleteListUndoSnackbar(requireActivity() as AppCompatActivity, it) { updateLists() } + ReadingListBehaviorsUtil.deleteReadingList(requireActivity(), it, false) { + ReadingListBehaviorsUtil.showDeleteListUndoSnackbar(requireActivity(), it) { updateLists() } updateLists() } } @@ -708,8 +708,8 @@ class ReadingListsFragment : Fragment(), SortReadingListsDialog.Callback, Readin override fun onDeleteSelected() { selectedLists.let { - ReadingListBehaviorsUtil.deleteReadingLists(requireActivity() as AppCompatActivity, it) { - ReadingListBehaviorsUtil.showDeleteListsUndoSnackbar(requireActivity() as AppCompatActivity, it) { updateLists() } + ReadingListBehaviorsUtil.deleteReadingLists(requireActivity(), it) { + ReadingListBehaviorsUtil.showDeleteListsUndoSnackbar(requireActivity(), it) { updateLists() } finishActionMode() updateLists() } diff --git a/app/src/main/java/org/wikipedia/readinglist/RemoveFromReadingListsDialog.kt b/app/src/main/java/org/wikipedia/readinglist/RemoveFromReadingListsDialog.kt index 9fd6894ac7a..54a36ae0898 100644 --- a/app/src/main/java/org/wikipedia/readinglist/RemoveFromReadingListsDialog.kt +++ b/app/src/main/java/org/wikipedia/readinglist/RemoveFromReadingListsDialog.kt @@ -1,9 +1,9 @@ package org.wikipedia.readinglist -import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.lifecycleScope +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 @@ -20,12 +20,12 @@ class RemoveFromReadingListsDialog(private val listsContainingPage: List, ReadingList.SORT_BY_NAME_ASC) } - fun deleteOrShowDialog(activity: AppCompatActivity, callback: Callback?) { + fun deleteOrShowDialog(activity: Activity, callback: Callback?) { if (listsContainingPage.isEmpty()) { return } if (listsContainingPage.size == 1 && listsContainingPage[0].pages.isNotEmpty()) { - activity.lifecycleScope.launch(CoroutineExceptionHandler { _, throwable -> + MainScope().launch(CoroutineExceptionHandler { _, throwable -> L.w(throwable) }) { AppDatabase.instance.readingListPageDao().markPagesForDeletion(listsContainingPage[0], listOf(listsContainingPage[0].pages[0])) @@ -36,12 +36,12 @@ class RemoveFromReadingListsDialog(private val listsContainingPage: List - activity.lifecycleScope.launch(CoroutineExceptionHandler { _, throwable -> + MainScope().launch(CoroutineExceptionHandler { _, throwable -> L.w(throwable) }) { val newLists = (listsContainingPage zip selectedLists.asIterable())