diff --git a/app/schemas/org.wikipedia.database.AppDatabase/32.json b/app/schemas/org.wikipedia.database.AppDatabase/32.json new file mode 100644 index 00000000000..c32a574cef2 --- /dev/null +++ b/app/schemas/org.wikipedia.database.AppDatabase/32.json @@ -0,0 +1,851 @@ +{ + "formatVersion": 1, + "database": { + "version": 32, + "identityHash": "8e82196ec225f2b0a698fdb81b327adf", + "entities": [ + { + "tableName": "HistoryEntry", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`authority` TEXT NOT NULL, `lang` TEXT NOT NULL, `apiTitle` TEXT NOT NULL, `displayTitle` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `namespace` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `source` INTEGER NOT NULL, `prevId` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "authority", + "columnName": "authority", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lang", + "columnName": "lang", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "apiTitle", + "columnName": "apiTitle", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayTitle", + "columnName": "displayTitle", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "namespace", + "columnName": "namespace", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "prevId", + "columnName": "prevId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_HistoryEntry_lang_namespace_apiTitle", + "unique": false, + "columnNames": [ + "lang", + "namespace", + "apiTitle" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_HistoryEntry_lang_namespace_apiTitle` ON `${TABLE_NAME}` (`lang`, `namespace`, `apiTitle`)" + } + ] + }, + { + "tableName": "PageImage", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`lang` TEXT NOT NULL, `namespace` TEXT NOT NULL, `apiTitle` TEXT NOT NULL, `imageName` TEXT, `description` TEXT, `timeSpentSec` INTEGER NOT NULL, `geoLat` REAL NOT NULL, `geoLon` REAL NOT NULL, PRIMARY KEY(`lang`, `namespace`, `apiTitle`))", + "fields": [ + { + "fieldPath": "lang", + "columnName": "lang", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "namespace", + "columnName": "namespace", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "apiTitle", + "columnName": "apiTitle", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "imageName", + "columnName": "imageName", + "affinity": "TEXT" + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT" + }, + { + "fieldPath": "timeSpentSec", + "columnName": "timeSpentSec", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "geoLat", + "columnName": "geoLat", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "geoLon", + "columnName": "geoLon", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "lang", + "namespace", + "apiTitle" + ] + }, + "indices": [ + { + "name": "index_PageImage_lang_namespace_apiTitle", + "unique": false, + "columnNames": [ + "lang", + "namespace", + "apiTitle" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_PageImage_lang_namespace_apiTitle` ON `${TABLE_NAME}` (`lang`, `namespace`, `apiTitle`)" + } + ] + }, + { + "tableName": "RecentSearch", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`text` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`text`))", + "fields": [ + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "text" + ] + } + }, + { + "tableName": "TalkPageSeen", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sha` TEXT NOT NULL, PRIMARY KEY(`sha`))", + "fields": [ + { + "fieldPath": "sha", + "columnName": "sha", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sha" + ] + } + }, + { + "tableName": "EditSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`summary` TEXT NOT NULL, `lastUsed` INTEGER NOT NULL, PRIMARY KEY(`summary`))", + "fields": [ + { + "fieldPath": "summary", + "columnName": "summary", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastUsed", + "columnName": "lastUsed", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "summary" + ] + } + }, + { + "tableName": "OfflineObject", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `lang` TEXT NOT NULL, `path` TEXT NOT NULL, `status` INTEGER NOT NULL, `usedByStr` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lang", + "columnName": "lang", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "usedByStr", + "columnName": "usedByStr", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "ReadingList", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`listTitle` TEXT NOT NULL, `description` TEXT, `mtime` INTEGER NOT NULL, `atime` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `sizeBytes` INTEGER NOT NULL, `dirty` INTEGER NOT NULL, `remoteId` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "listTitle", + "columnName": "listTitle", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT" + }, + { + "fieldPath": "mtime", + "columnName": "mtime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "atime", + "columnName": "atime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sizeBytes", + "columnName": "sizeBytes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dirty", + "columnName": "dirty", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteId", + "columnName": "remoteId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "ReadingListPage", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`wiki` TEXT NOT NULL, `namespace` INTEGER NOT NULL, `displayTitle` TEXT NOT NULL, `apiTitle` TEXT NOT NULL, `description` TEXT, `thumbUrl` TEXT, `listId` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `mtime` INTEGER NOT NULL, `atime` INTEGER NOT NULL, `offline` INTEGER NOT NULL, `status` INTEGER NOT NULL, `sizeBytes` INTEGER NOT NULL, `lang` TEXT NOT NULL, `revId` INTEGER NOT NULL, `remoteId` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "wiki", + "columnName": "wiki", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "namespace", + "columnName": "namespace", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayTitle", + "columnName": "displayTitle", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "apiTitle", + "columnName": "apiTitle", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT" + }, + { + "fieldPath": "thumbUrl", + "columnName": "thumbUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "listId", + "columnName": "listId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mtime", + "columnName": "mtime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "atime", + "columnName": "atime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "offline", + "columnName": "offline", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sizeBytes", + "columnName": "sizeBytes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lang", + "columnName": "lang", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "revId", + "columnName": "revId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteId", + "columnName": "remoteId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "Notification", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `wiki` TEXT NOT NULL, `read` TEXT, `category` TEXT NOT NULL, `type` TEXT NOT NULL, `revid` INTEGER NOT NULL, `title` TEXT, `agent` TEXT, `timestamp` TEXT, `contents` TEXT, PRIMARY KEY(`id`, `wiki`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wiki", + "columnName": "wiki", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "TEXT" + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "revid", + "columnName": "revid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT" + }, + { + "fieldPath": "agent", + "columnName": "agent", + "affinity": "TEXT" + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "TEXT" + }, + { + "fieldPath": "contents", + "columnName": "contents", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "wiki" + ] + } + }, + { + "tableName": "TalkTemplate", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` INTEGER NOT NULL, `order` INTEGER NOT NULL, `title` TEXT NOT NULL, `subject` TEXT NOT NULL, `message` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "Category", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`year` INTEGER NOT NULL, `month` INTEGER NOT NULL, `title` TEXT NOT NULL, `lang` TEXT NOT NULL, `count` INTEGER NOT NULL, PRIMARY KEY(`year`, `month`, `title`, `lang`))", + "fields": [ + { + "fieldPath": "year", + "columnName": "year", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "month", + "columnName": "month", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lang", + "columnName": "lang", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "count", + "columnName": "count", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "year", + "month", + "title", + "lang" + ] + } + }, + { + "tableName": "DailyGameHistory", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `gameName` INTEGER NOT NULL, `language` TEXT NOT NULL, `year` INTEGER NOT NULL, `month` INTEGER NOT NULL, `day` INTEGER NOT NULL, `score` INTEGER NOT NULL, `playType` INTEGER NOT NULL, `gameData` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gameName", + "columnName": "gameName", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "month", + "columnName": "month", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "day", + "columnName": "day", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "score", + "columnName": "score", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playType", + "columnName": "playType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gameData", + "columnName": "gameData", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "RecommendedPage", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `wiki` TEXT NOT NULL, `lang` TEXT NOT NULL, `namespace` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `apiTitle` TEXT NOT NULL, `displayTitle` TEXT NOT NULL, `description` TEXT, `thumbUrl` TEXT, `status` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wiki", + "columnName": "wiki", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lang", + "columnName": "lang", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "namespace", + "columnName": "namespace", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "apiTitle", + "columnName": "apiTitle", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayTitle", + "columnName": "displayTitle", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT" + }, + { + "fieldPath": "thumbUrl", + "columnName": "thumbUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "Tab", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `order` INTEGER NOT NULL, `backStackIds` TEXT NOT NULL, `backStackPosition` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "backStackIds", + "columnName": "backStackIds", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "backStackPosition", + "columnName": "backStackPosition", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "PageBackStackItem", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `apiTitle` TEXT NOT NULL, `displayTitle` TEXT NOT NULL, `langCode` TEXT NOT NULL, `namespace` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `scrollY` INTEGER NOT NULL, `source` INTEGER NOT NULL, `thumbUrl` TEXT, `description` TEXT, `extract` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "apiTitle", + "columnName": "apiTitle", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayTitle", + "columnName": "displayTitle", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "langCode", + "columnName": "langCode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "namespace", + "columnName": "namespace", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scrollY", + "columnName": "scrollY", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thumbUrl", + "columnName": "thumbUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT" + }, + { + "fieldPath": "extract", + "columnName": "extract", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8e82196ec225f2b0a698fdb81b327adf')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/org/wikipedia/WikipediaApp.kt b/app/src/main/java/org/wikipedia/WikipediaApp.kt index 1f70072bc6b..76e13b8cbca 100644 --- a/app/src/main/java/org/wikipedia/WikipediaApp.kt +++ b/app/src/main/java/org/wikipedia/WikipediaApp.kt @@ -29,7 +29,6 @@ import org.wikipedia.language.AcceptLanguageUtil import org.wikipedia.language.AppLanguageState import org.wikipedia.notifications.NotificationCategory import org.wikipedia.notifications.NotificationPollBroadcastReceiver -import org.wikipedia.page.tabs.Tab import org.wikipedia.push.WikipediaFirebaseMessagingService import org.wikipedia.settings.Prefs import org.wikipedia.theme.Theme @@ -66,7 +65,6 @@ class WikipediaApp : Application() { private var defaultWikiSite: WikiSite? = null val connectionStateMonitor = ConnectionStateMonitor() - val tabList = mutableListOf() var currentTheme = Theme.fallback set(value) { @@ -112,10 +110,6 @@ class WikipediaApp : Application() { return defaultWikiSite!! } - // handle the case where we have a single tab with an empty backstack, which shouldn't count as a valid tab: - val tabCount - get() = if (tabList.size > 1) tabList.size else if (tabList.isEmpty()) 0 else if (tabList[0].backStack.isEmpty()) 0 else tabList.size - val isOnline get() = connectionStateMonitor.isOnline() @@ -149,7 +143,6 @@ class WikipediaApp : Application() { currentTheme = unmarshalTheme(Prefs.currentThemeId) - initTabs() enableWebViewDebugging() registerActivityLifecycleCallbacks(activityLifecycleHandler) registerComponentCallbacks(activityLifecycleHandler) @@ -211,15 +204,6 @@ class WikipediaApp : Application() { // TODO: send exception to custom crash reporting system } - fun commitTabState() { - if (tabList.isEmpty()) { - Prefs.clearTabs() - initTabs() - } else { - Prefs.tabs = tabList - } - } - /** * Gets the current size of the app's font. This is given as a device-specific size (not "sp"), * and can be passed directly to setTextSize() functions. @@ -280,15 +264,6 @@ class WikipediaApp : Application() { return result } - private fun initTabs() { - if (Prefs.hasTabs) { - tabList.addAll(Prefs.tabs) - } - if (tabList.isEmpty()) { - tabList.add(Tab()) - } - } - companion object { lateinit var instance: WikipediaApp private set diff --git a/app/src/main/java/org/wikipedia/database/AppDatabase.kt b/app/src/main/java/org/wikipedia/database/AppDatabase.kt index 49d7837f052..43952f06c54 100644 --- a/app/src/main/java/org/wikipedia/database/AppDatabase.kt +++ b/app/src/main/java/org/wikipedia/database/AppDatabase.kt @@ -20,6 +20,10 @@ import org.wikipedia.notifications.db.Notification import org.wikipedia.notifications.db.NotificationDao import org.wikipedia.offline.db.OfflineObject import org.wikipedia.offline.db.OfflineObjectDao +import org.wikipedia.page.tabs.PageBackStackItem +import org.wikipedia.page.tabs.PageBackStackItemDao +import org.wikipedia.page.tabs.Tab +import org.wikipedia.page.tabs.TabDao import org.wikipedia.pageimages.db.PageImage import org.wikipedia.pageimages.db.PageImageDao import org.wikipedia.readinglist.database.ReadingList @@ -30,6 +34,7 @@ import org.wikipedia.readinglist.db.ReadingListPageDao import org.wikipedia.readinglist.db.RecommendedPageDao import org.wikipedia.search.db.RecentSearch import org.wikipedia.search.db.RecentSearchDao +import org.wikipedia.settings.Prefs import org.wikipedia.staticdata.MainPageNameData import org.wikipedia.talk.db.TalkPageSeen import org.wikipedia.talk.db.TalkPageSeenDao @@ -38,7 +43,7 @@ import org.wikipedia.talk.db.TalkTemplateDao import java.time.LocalDate const val DATABASE_NAME = "wikipedia.db" -const val DATABASE_VERSION = 31 +const val DATABASE_VERSION = 32 @Database( entities = [ @@ -54,7 +59,9 @@ const val DATABASE_VERSION = 31 TalkTemplate::class, Category::class, DailyGameHistory::class, - RecommendedPage::class + RecommendedPage::class, + Tab::class, + PageBackStackItem::class ], version = DATABASE_VERSION ) @@ -80,6 +87,8 @@ abstract class AppDatabase : RoomDatabase() { abstract fun categoryDao(): CategoryDao abstract fun dailyGameHistoryDao(): DailyGameHistoryDao abstract fun recommendedPageDao(): RecommendedPageDao + abstract fun tabDao(): TabDao + abstract fun pageBackStackItemDao(): PageBackStackItemDao companion object { val MIGRATION_19_20 = object : Migration(19, 20) { @@ -347,13 +356,65 @@ abstract class AppDatabase : RoomDatabase() { db.execSQL("ALTER TABLE Category_temp RENAME TO Category") } } + val MIGRATION_31_32 = object : Migration(31, 32) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("CREATE TABLE IF NOT EXISTS `Tab` (" + + " `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," + + " `order` INTEGER NOT NULL," + + " `backStackIds` TEXT NOT NULL," + + " `backStackPosition` INTEGER NOT NULL" + + ")") + db.execSQL("CREATE TABLE IF NOT EXISTS `PageBackStackItem` (" + + " `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," + + " `apiTitle` TEXT NOT NULL," + + " `displayTitle` TEXT NOT NULL," + + " `langCode` TEXT NOT NULL," + + " `namespace` TEXT NOT NULL," + + " `timestamp` INTEGER NOT NULL," + + " `scrollY` INTEGER NOT NULL," + + " `source` INTEGER NOT NULL," + + " `thumbUrl` TEXT," + + " `description` TEXT," + + " `extract` TEXT" + + ")") + + // Migrating from Pres.tabs to database + var pageBackStackItemIndex = 1 + Prefs.tabs.forEachIndexed { index, tab -> + // Insert back stack items to PageBackStackItem and get the IDs + var backStackIds = mutableListOf() + // TODO: make sure the order is correct + tab.backStack.forEach { backStackItem -> + db.execSQL("INSERT INTO PageBackStackItem " + + "(apiTitle, displayTitle, langCode, namespace, timestamp, scrollY, source, thumbUrl, description, extract) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + arrayOf( + backStackItem.apiTitle, + backStackItem.displayTitle, + backStackItem.langCode, + backStackItem.namespace, + backStackItem.timestamp, + backStackItem.scrollY, + backStackItem.source, + backStackItem.thumbUrl, + backStackItem.description, + backStackItem.extract + )) + backStackIds.add(pageBackStackItemIndex++) + } + // Insert the tab into the Tab table + db.execSQL("INSERT INTO Tab (`order`, backStackIds, backStackPosition) VALUES (?, ?, ?)", + arrayOf(index, backStackIds.joinToString(","), tab.backStackPosition)) + } + } + } val instance: AppDatabase by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { Room.databaseBuilder(WikipediaApp.instance, AppDatabase::class.java, DATABASE_NAME) .addMigrations(MIGRATION_19_20, MIGRATION_20_21, MIGRATION_21_22, MIGRATION_22_23, 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) + MIGRATION_30_31, MIGRATION_31_32) .fallbackToDestructiveMigration(false) .build() } diff --git a/app/src/main/java/org/wikipedia/feed/news/NewsFragment.kt b/app/src/main/java/org/wikipedia/feed/news/NewsFragment.kt index 27ddf2d1a49..31c825c8ed0 100644 --- a/app/src/main/java/org/wikipedia/feed/news/NewsFragment.kt +++ b/app/src/main/java/org/wikipedia/feed/news/NewsFragment.kt @@ -12,6 +12,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.appbar.AppBarLayout import org.wikipedia.Constants @@ -24,6 +25,7 @@ import org.wikipedia.feed.model.Card import org.wikipedia.feed.view.ListCardItemView import org.wikipedia.history.HistoryEntry import org.wikipedia.page.PageActivity +import org.wikipedia.page.tabs.TabHelper import org.wikipedia.readinglist.ReadingListBehaviorsUtil import org.wikipedia.richtext.RichTextUtil import org.wikipedia.util.DeviceUtil @@ -31,7 +33,6 @@ import org.wikipedia.util.DimenUtil import org.wikipedia.util.FeedbackUtil import org.wikipedia.util.GradientUtil import org.wikipedia.util.ResourceUtil -import org.wikipedia.util.TabUtil import org.wikipedia.views.DefaultRecyclerAdapter import org.wikipedia.views.DefaultViewHolder import org.wikipedia.views.DrawableItemDecoration @@ -109,7 +110,7 @@ class NewsFragment : Fragment() { private inner class Callback : ListCardItemView.Callback { override fun onSelectPage(card: Card, entry: HistoryEntry, openInNewBackgroundTab: Boolean) { if (openInNewBackgroundTab) { - TabUtil.openInNewBackgroundTab(entry) + TabHelper.openInNewBackgroundTab(viewLifecycleOwner.lifecycleScope, entry) FeedbackUtil.showMessage(requireActivity(), R.string.article_opened_in_background_tab) } else { startActivity(PageActivity.newIntentForNewTab(requireContext(), entry, entry.title)) diff --git a/app/src/main/java/org/wikipedia/feed/onthisday/OnThisDayPagesViewHolder.kt b/app/src/main/java/org/wikipedia/feed/onthisday/OnThisDayPagesViewHolder.kt index 93477addc91..25abf65a09c 100644 --- a/app/src/main/java/org/wikipedia/feed/onthisday/OnThisDayPagesViewHolder.kt +++ b/app/src/main/java/org/wikipedia/feed/onthisday/OnThisDayPagesViewHolder.kt @@ -6,6 +6,7 @@ import android.view.View import android.widget.FrameLayout import android.widget.TextView import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import org.wikipedia.Constants import org.wikipedia.R @@ -13,6 +14,7 @@ import org.wikipedia.dataclient.WikiSite import org.wikipedia.dataclient.page.PageSummary import org.wikipedia.history.HistoryEntry import org.wikipedia.page.PageActivity +import org.wikipedia.page.tabs.TabHelper import org.wikipedia.readinglist.LongPressMenu import org.wikipedia.readinglist.ReadingListBehaviorsUtil import org.wikipedia.readinglist.database.ReadingListPage @@ -20,7 +22,6 @@ import org.wikipedia.util.DeviceUtil import org.wikipedia.util.DimenUtil import org.wikipedia.util.FeedbackUtil import org.wikipedia.util.StringUtil -import org.wikipedia.util.TabUtil import org.wikipedia.util.TransitionUtil import org.wikipedia.views.FaceAndColorDetectImageView @@ -92,7 +93,7 @@ class OnThisDayPagesViewHolder( } override fun onOpenInNewTab(entry: HistoryEntry) { - TabUtil.openInNewBackgroundTab(entry) + TabHelper.openInNewBackgroundTab(activity.lifecycleScope, entry) FeedbackUtil.showMessage(activity, R.string.article_opened_in_background_tab) } diff --git a/app/src/main/java/org/wikipedia/feed/topread/TopReadFragment.kt b/app/src/main/java/org/wikipedia/feed/topread/TopReadFragment.kt index 84ea93e8407..5808b910bb5 100644 --- a/app/src/main/java/org/wikipedia/feed/topread/TopReadFragment.kt +++ b/app/src/main/java/org/wikipedia/feed/topread/TopReadFragment.kt @@ -10,6 +10,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import org.wikipedia.Constants import org.wikipedia.Constants.InvokeSource @@ -20,10 +21,10 @@ import org.wikipedia.feed.model.Card import org.wikipedia.feed.view.ListCardItemView import org.wikipedia.history.HistoryEntry import org.wikipedia.page.PageActivity +import org.wikipedia.page.tabs.TabHelper import org.wikipedia.readinglist.ReadingListBehaviorsUtil import org.wikipedia.util.DimenUtil import org.wikipedia.util.FeedbackUtil -import org.wikipedia.util.TabUtil import org.wikipedia.views.DefaultRecyclerAdapter import org.wikipedia.views.DefaultViewHolder import org.wikipedia.views.DrawableItemDecoration @@ -77,7 +78,7 @@ class TopReadFragment : Fragment() { private inner class Callback : ListCardItemView.Callback { override fun onSelectPage(card: Card, entry: HistoryEntry, openInNewBackgroundTab: Boolean) { if (openInNewBackgroundTab) { - TabUtil.openInNewBackgroundTab(entry) + TabHelper.openInNewBackgroundTab(viewLifecycleOwner.lifecycleScope, entry) FeedbackUtil.showMessage(requireActivity(), R.string.article_opened_in_background_tab) } else { startActivity(PageActivity.newIntentForNewTab(requireContext(), entry, entry.title)) diff --git a/app/src/main/java/org/wikipedia/json/PageBackStackItemSerializer.kt b/app/src/main/java/org/wikipedia/json/PageBackStackItemSerializer.kt new file mode 100644 index 00000000000..473a616911e --- /dev/null +++ b/app/src/main/java/org/wikipedia/json/PageBackStackItemSerializer.kt @@ -0,0 +1,45 @@ +package org.wikipedia.json + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.jsonObject +import org.wikipedia.history.HistoryEntry +import org.wikipedia.page.PageTitle +import org.wikipedia.page.tabs.PageBackStackItem + +// TODO: remove on 2026-07-01 +object PageBackStackItemSerializer : KSerializer { + override val descriptor = buildClassSerialDescriptor("PageBackStackItem") + + override fun deserialize(decoder: Decoder): PageBackStackItem { + val input = decoder as JsonDecoder + val element = input.decodeJsonElement().jsonObject + + // This is to handle the old format of PageBackStackItem which originally stored in the Prefs.tabs + return if (element.containsKey("title") && element.containsKey("historyEntry")) { + val oldFormat = Json.decodeFromJsonElement(element) + PageBackStackItem(oldFormat.title, oldFormat.historyEntry).also { + it.scrollY = oldFormat.scrollY + } + } else { + Json.decodeFromJsonElement(element) + } + } + + override fun serialize(encoder: Encoder, value: PageBackStackItem) { + encoder.encodeSerializableValue(PageBackStackItem.serializer(), value) + } +} + +@Serializable +private data class OldPageBackStackItem( + val title: PageTitle, + val historyEntry: HistoryEntry, + val scrollY: Int = 0 +) diff --git a/app/src/main/java/org/wikipedia/main/MainFragment.kt b/app/src/main/java/org/wikipedia/main/MainFragment.kt index a1f89582de0..b674abb379b 100644 --- a/app/src/main/java/org/wikipedia/main/MainFragment.kt +++ b/app/src/main/java/org/wikipedia/main/MainFragment.kt @@ -65,6 +65,7 @@ import org.wikipedia.page.ExclusiveBottomSheetPresenter import org.wikipedia.page.PageActivity import org.wikipedia.page.PageTitle import org.wikipedia.page.tabs.TabActivity +import org.wikipedia.page.tabs.TabHelper import org.wikipedia.places.PlacesActivity import org.wikipedia.random.RandomActivity import org.wikipedia.readinglist.ReadingListBehaviorsUtil @@ -82,7 +83,6 @@ import org.wikipedia.usercontrib.UserContribListActivity import org.wikipedia.util.DimenUtil import org.wikipedia.util.FeedbackUtil import org.wikipedia.util.ShareUtil -import org.wikipedia.util.TabUtil import org.wikipedia.views.NotificationButtonView import org.wikipedia.views.TabCountsView import org.wikipedia.views.imageservice.ImageService @@ -216,7 +216,7 @@ class MainFragment : Fragment(), BackPressedHandler, MenuProvider, FeedFragment. FeedbackUtil.showMessage(this, R.string.login_success_toast) } } else if (requestCode == Constants.ACTIVITY_REQUEST_BROWSE_TABS) { - if (WikipediaApp.instance.tabCount == 0) { + if (TabHelper.count == 0) { // They browsed the tabs and cleared all of them, without wanting to open a new tab. return } @@ -275,14 +275,14 @@ class MainFragment : Fragment(), BackPressedHandler, MenuProvider, FeedFragment. menu.findItem(R.id.menu_overflow_button).isVisible = currentFragment is ReadingListsFragment val tabsItem = menu.findItem(R.id.menu_tabs) - if (WikipediaApp.instance.tabCount < 1 || currentFragment is SuggestedEditsTasksFragment) { + if (TabHelper.count < 1 || currentFragment is SuggestedEditsTasksFragment) { tabsItem.isVisible = false tabCountsView = null } else { tabsItem.isVisible = true tabCountsView = TabCountsView(requireActivity(), null) tabCountsView!!.setOnClickListener { - if (WikipediaApp.instance.tabCount == 1) { + if (TabHelper.count == 1) { startActivity(PageActivity.newIntent(requireActivity())) } else { startActivityForResult(TabActivity.newIntent(requireActivity()), Constants.ACTIVITY_REQUEST_BROWSE_TABS) @@ -333,7 +333,7 @@ class MainFragment : Fragment(), BackPressedHandler, MenuProvider, FeedFragment. goToTab(NavTab.of(intent.getIntExtra(Constants.INTENT_EXTRA_GO_TO_SE_TAB, NavTab.EDITS.code()))) } else if (intent.hasExtra(Constants.INTENT_EXTRA_PREVIEW_SAVED_READING_LISTS)) { goToTab(NavTab.READING_LISTS) - } else if (lastPageViewedWithin(1) && !intent.hasExtra(Constants.INTENT_RETURN_TO_MAIN) && WikipediaApp.instance.tabCount > 0) { + } else if (lastPageViewedWithin(1) && !intent.hasExtra(Constants.INTENT_RETURN_TO_MAIN) && TabHelper.count > 0) { startActivity(PageActivity.newIntent(requireContext())) } } @@ -354,9 +354,10 @@ class MainFragment : Fragment(), BackPressedHandler, MenuProvider, FeedFragment. override fun onFeedSelectPage(entry: HistoryEntry, openInNewBackgroundTab: Boolean) { if (openInNewBackgroundTab) { - TabUtil.openInNewBackgroundTab(entry) - showTabCountsAnimation = true - requireActivity().invalidateOptionsMenu() + TabHelper.openInNewBackgroundTab(viewLifecycleOwner.lifecycleScope, entry) { + showTabCountsAnimation = true + requireActivity().invalidateOptionsMenu() + } } else { startActivity(PageActivity.newIntentForNewTab(requireContext(), entry, entry.title)) } diff --git a/app/src/main/java/org/wikipedia/page/PageActivity.kt b/app/src/main/java/org/wikipedia/page/PageActivity.kt index 6d745ffcb34..bb890ed539a 100644 --- a/app/src/main/java/org/wikipedia/page/PageActivity.kt +++ b/app/src/main/java/org/wikipedia/page/PageActivity.kt @@ -34,6 +34,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import org.wikipedia.Constants import org.wikipedia.Constants.InvokeSource import org.wikipedia.R @@ -65,6 +66,7 @@ import org.wikipedia.notifications.AnonymousNotificationHelper import org.wikipedia.notifications.NotificationActivity import org.wikipedia.page.linkpreview.LinkPreviewDialog import org.wikipedia.page.tabs.TabActivity +import org.wikipedia.page.tabs.TabHelper import org.wikipedia.readinglist.ReadingListActivity import org.wikipedia.readinglist.ReadingListMode import org.wikipedia.search.SearchActivity @@ -142,7 +144,7 @@ class PageActivity : BaseActivity(), PageFragment.Callback, LinkPreviewDialog.Lo } private val requestBrowseTabLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - if (app.tabCount == 0 && it.resultCode != TabActivity.RESULT_NEW_TAB) { + if (TabHelper.count == 0 && it.resultCode != TabActivity.RESULT_NEW_TAB) { // They browsed the tabs and cleared all of them, without wanting to open a new tab. finish() return@registerForActivityResult @@ -151,6 +153,7 @@ class PageActivity : BaseActivity(), PageFragment.Callback, LinkPreviewDialog.Lo loadMainPage(TabPosition.NEW_TAB_FOREGROUND) animateTabsButton() } else if (it.resultCode == TabActivity.RESULT_LOAD_FROM_BACKSTACK) { + pageFragment.initTab() pageFragment.reloadFromBackstack(false) } } @@ -413,6 +416,16 @@ class PageActivity : BaseActivity(), PageFragment.Callback, LinkPreviewDialog.Lo override fun onPageLoadComplete() { removeTransitionAnimState() maybeShowThemeTooltip() + runBlocking { + L.d("onPageLoadComplete " + pageFragment.currentTab.id) + L.d("onPageLoadComplete size " + pageFragment.currentTab.backStack.size) + if (pageFragment.currentTab.id != 0L) { + TabHelper.updateTab(pageFragment.currentTab) + } else { + TabHelper.insertTabs(listOf(pageFragment.currentTab)) + } + pageFragment.setTab(TabHelper.getCurrentTab()) + } } override fun onPageDismissBottomSheet() { @@ -639,7 +652,7 @@ class PageActivity : BaseActivity(), PageFragment.Callback, LinkPreviewDialog.Lo private fun loadFilePageFromBackStackIfNeeded() { if (pageFragment.currentTab.backStack.isNotEmpty()) { val item = pageFragment.currentTab.backStack[pageFragment.currentTab.backStackPosition] - loadNonArticlePageIfNeeded(item.title) + loadNonArticlePageIfNeeded(item.getPageTitle()) } } diff --git a/app/src/main/java/org/wikipedia/page/PageBackStackItem.kt b/app/src/main/java/org/wikipedia/page/PageBackStackItem.kt deleted file mode 100644 index 4a833c864b5..00000000000 --- a/app/src/main/java/org/wikipedia/page/PageBackStackItem.kt +++ /dev/null @@ -1,7 +0,0 @@ -package org.wikipedia.page - -import kotlinx.serialization.Serializable -import org.wikipedia.history.HistoryEntry - -@Serializable -class PageBackStackItem(var title: PageTitle, var historyEntry: HistoryEntry, var scrollY: Int = 0) diff --git a/app/src/main/java/org/wikipedia/page/PageFragment.kt b/app/src/main/java/org/wikipedia/page/PageFragment.kt index 17e8580ec35..f4495e3deab 100644 --- a/app/src/main/java/org/wikipedia/page/PageFragment.kt +++ b/app/src/main/java/org/wikipedia/page/PageFragment.kt @@ -37,6 +37,7 @@ import com.google.android.material.textview.MaterialTextView import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.float import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject @@ -92,7 +93,9 @@ import org.wikipedia.page.leadimages.LeadImagesHandler import org.wikipedia.page.references.PageReferences import org.wikipedia.page.references.ReferenceDialog import org.wikipedia.page.shareafact.ShareHandler +import org.wikipedia.page.tabs.PageBackStackItem import org.wikipedia.page.tabs.Tab +import org.wikipedia.page.tabs.TabHelper import org.wikipedia.places.PlacesActivity import org.wikipedia.readinglist.LongPressMenu import org.wikipedia.readinglist.ReadingListBehaviorsUtil @@ -184,13 +187,11 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi lateinit var sidePanelHandler: SidePanelHandler lateinit var shareHandler: ShareHandler lateinit var editHandler: EditHandler + var currentTab = Tab() + var revision = 0L - private val shouldCreateNewTab get() = currentTab.backStack.isNotEmpty() - private val backgroundTabPosition get() = 0.coerceAtLeast(foregroundTabPosition - 1) - private val foregroundTabPosition get() = app.tabList.size private val tabLayoutOffsetParams get() = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, binding.pageActionsTabLayout.height) - val currentTab get() = app.tabList.last() val title get() = model.title val page get() = model.page val isLoading get() = bridge.isLoading @@ -200,6 +201,7 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi _binding = FragmentPageBinding.inflate(inflater, container, false) webView = binding.pageWebView initWebViewListeners() + initTab() binding.pageRefreshContainer.scrollableChild = webView binding.pageRefreshContainer.setOnRefreshListener(pageRefreshListener) val swipeOffset = DimenUtil.getContentTopOffsetPx(requireActivity()) + REFRESH_SPINNER_ADDITIONAL_OFFSET @@ -292,8 +294,10 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi activeTimer.pause() addTimeSpentReading(activeTimer.elapsedSec) pageFragmentLoadState.updateCurrentBackStackItem() - app.commitTabState() - val time = if (app.tabList.size >= 1 && !pageFragmentLoadState.backStackEmpty()) System.currentTimeMillis() else 0 + runBlocking { + TabHelper.updateTab(currentTab) + } + val time = if (currentTab.backStack.isNotEmpty() && !pageFragmentLoadState.backStackEmpty()) System.currentTimeMillis() else 0 Prefs.pageLastShown = time articleInteractionEvent?.pause() metricsPlatformArticleEventToolbarInteraction.pause() @@ -335,10 +339,7 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi return true } // if the current tab can no longer go back, then close the tab before exiting - if (app.tabList.isNotEmpty()) { - app.tabList.removeAt(app.tabList.size - 1) - app.commitTabState() - } + TabHelper.removeTab(currentTab) return false } @@ -527,59 +528,59 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi } } - private fun setCurrentTabAndReset(position: Int) { - // move the selected tab to the bottom of the list, and navigate to it! + private fun setCurrentTabAndReset(tab: Tab) { + L.d("setCurrentTabAndReset" + tab.id + " " + tab.backStack.size) + // move the selected tab to the foreground, and navigate to it! // (but only if it's a different tab than the one currently in view! - if (position < app.tabList.size - 1) { - val tab = app.tabList.removeAt(position) - app.tabList.add(tab) - pageFragmentLoadState.setTab(tab) + // TODO: verify this & make sure to use coroutine + runBlocking { + pageFragmentLoadState.setTab(TabHelper.moveTabToForeground(tab).first()) } - if (app.tabCount > 0) { - app.tabList.last().squashBackstack() + if (TabHelper.count > 0) { + // TODO: verify this (whether it is a foreground tab or not) + currentTab.squashBackstack() pageFragmentLoadState.loadFromBackStack() } } - private fun selectedTabPosition(title: PageTitle): Int { - return app.tabList.firstOrNull { it.backStackPositionTitle != null && - title == it.backStackPositionTitle }?.let { app.tabList.indexOf(it) } ?: -1 + fun initTab() { + // TODO: use coroutines if we have viewModel + runBlocking { + currentTab = TabHelper.getCurrentTab() + } + } + + fun setTab(tab: Tab) { + currentTab = tab + pageFragmentLoadState.setTab(tab) } - private fun openInNewTab(title: PageTitle, entry: HistoryEntry, position: Int) { - val selectedTabPosition = selectedTabPosition(title) - if (selectedTabPosition >= 0) { - setCurrentTabAndReset(selectedTabPosition) + private fun openInNewTab(title: PageTitle, entry: HistoryEntry, toForeground: Boolean) { + val selectedTab = TabHelper.findTabByTitle(title) + if (selectedTab != null) { + setCurrentTabAndReset(selectedTab) return } - if (shouldCreateNewTab) { - // create a new tab - val tab = Tab() - val isForeground = position == foregroundTabPosition - // if the requested position is at the top, then make its backstack current - if (isForeground) { - pageFragmentLoadState.setTab(tab) - } - // put this tab in the requested position - app.tabList.add(position, tab) - trimTabCount() - // add the requested page to its backstack - tab.backStack.add(PageBackStackItem(title, entry)) - if (!isForeground) { - lifecycleScope.launch(CoroutineExceptionHandler { _, t -> L.e(t) }) { - ServiceFactory.get(title.wikiSite).getInfoByPageIdsOrTitles(null, title.prefixedText) - .query?.firstPage()?.let { page -> - WikipediaApp.instance.tabList.find { it.backStackPositionTitle == title }?.backStackPositionTitle?.apply { - thumbUrl = page.thumbUrl() - description = page.description - } + + // TODO: handle this with coroutines if we have viewModel + runBlocking { + // Add a new PageBackStackItem to currentTab, which is an empty Tab + currentTab = Tab() + currentTab.backStack.add(PageBackStackItem(title, entry)) + if (!toForeground) { + ServiceFactory.get(title.wikiSite) + .getInfoByPageIdsOrTitles(null, title.prefixedText) + .query?.firstPage()?.let { page -> + // TODO: verify this + currentTab.getBackStackPositionTitle()?.apply { + thumbUrl = page.thumbUrl() + description = page.description } - } + pageFragmentLoadState.setTab(currentTab) + } } requireActivity().invalidateOptionsMenu() - } else { pageFragmentLoadState.setTab(currentTab) - currentTab.backStack.add(PageBackStackItem(title, entry)) } } @@ -599,12 +600,6 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi } } - private fun trimTabCount() { - while (app.tabList.size > Constants.MAX_TABS) { - app.tabList.removeAt(0) - } - } - private fun addTimeSpentReading(timeSpentSec: Int) { model.curEntry?.let { MainScope().launch(CoroutineExceptionHandler { _, throwable -> L.e(throwable) }) { @@ -906,6 +901,8 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi } fun reloadFromBackstack(forceReload: Boolean = true) { + // TODO: fix this + L.d("reloadFromBackstack ${currentTab.getBackStackPositionTitle()?.displayText}}") if (pageFragmentLoadState.setTab(currentTab) || forceReload) { if (!pageFragmentLoadState.backStackEmpty()) { pageFragmentLoadState.loadFromBackStack() @@ -960,37 +957,40 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi // handler), since the page metadata might have altered the lead image display state. bridge.execute(JavaScriptActionHandler.setTopMargin(leadImagesHandler.topMargin)) bridge.execute(JavaScriptActionHandler.setFooter(model)) + + // Update the currentTab from the pageLoadState + currentTab = pageFragmentLoadState.getCurrentTab() } fun openInNewBackgroundTab(title: PageTitle, entry: HistoryEntry) { - if (app.tabCount == 0) { - openInNewTab(title, entry, foregroundTabPosition) + if (TabHelper.count == 0) { + openInNewTab(title, entry, true) pageFragmentLoadState.loadFromBackStack() } else { - openInNewTab(title, entry, backgroundTabPosition) + openInNewTab(title, entry, false) (requireActivity() as PageActivity).animateTabsButton() } } fun openInNewForegroundTab(title: PageTitle, entry: HistoryEntry) { - openInNewTab(title, entry, foregroundTabPosition) + openInNewTab(title, entry, true) pageFragmentLoadState.loadFromBackStack() } fun openFromExistingTab(title: PageTitle, entry: HistoryEntry) { - val selectedTabPosition = selectedTabPosition(title) + val selectedTab = TabHelper.findTabByTitle(title) - if (selectedTabPosition == -1) { + if (selectedTab == null) { loadPage(title, entry, pushBackStack = true, squashBackstack = false) return } - setCurrentTabAndReset(selectedTabPosition) + setCurrentTabAndReset(selectedTab) } fun loadPage(title: PageTitle, entry: HistoryEntry, pushBackStack: Boolean, squashBackstack: Boolean, isRefresh: Boolean = false) { // is the new title the same as what's already being displayed? if (currentTab.backStack.isNotEmpty() && - title == currentTab.backStack[currentTab.backStackPosition].title) { + title == currentTab.backStack[currentTab.backStackPosition].getPageTitle()) { if (model.page == null || isRefresh) { pageFragmentLoadState.loadFromBackStack() } else if (!title.fragment.isNullOrEmpty()) { @@ -999,8 +999,9 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi return } if (squashBackstack) { - if (app.tabCount > 0) { - app.tabList.last().clearBackstack() + if (TabHelper.count > 0) { + // TODO: verify this + currentTab.clearBackstack() } } loadPage(title, entry, pushBackStack, 0, isRefresh) diff --git a/app/src/main/java/org/wikipedia/page/PageFragmentLoadState.kt b/app/src/main/java/org/wikipedia/page/PageFragmentLoadState.kt index 40e93e7f658..e9dc89d1812 100644 --- a/app/src/main/java/org/wikipedia/page/PageFragmentLoadState.kt +++ b/app/src/main/java/org/wikipedia/page/PageFragmentLoadState.kt @@ -22,6 +22,7 @@ import org.wikipedia.dataclient.page.PageSummary import org.wikipedia.history.HistoryEntry import org.wikipedia.notifications.AnonymousNotificationHelper import org.wikipedia.page.leadimages.LeadImagesHandler +import org.wikipedia.page.tabs.PageBackStackItem import org.wikipedia.page.tabs.Tab import org.wikipedia.settings.Prefs import org.wikipedia.staticdata.UserTalkAliasData @@ -58,8 +59,8 @@ class PageFragmentLoadState(private var model: PageViewModel, val item = currentTab.backStack[currentTab.backStackPosition] // display the page based on the backstack item, stage the scrollY position based on // the backstack item. - fragment.loadPage(item.title, item.historyEntry, false, item.scrollY) - L.d("Loaded page " + item.title.displayText + " from backstack") + fragment.loadPage(item.getPageTitle(), item.getHistoryEntry(), false, item.scrollY) + L.d("Loaded page " + item.displayTitle + " from backstack") } fun updateCurrentBackStackItem() { @@ -68,10 +69,8 @@ class PageFragmentLoadState(private var model: PageViewModel, } val item = currentTab.backStack[currentTab.backStackPosition] item.scrollY = webView.scrollY - model.title?.let { - item.title.description = it.description - item.title.thumbUrl = it.thumbUrl - } + item.description = model.title?.description + item.thumbUrl = model.title?.thumbUrl } fun setTab(tab: Tab): Boolean { @@ -80,6 +79,10 @@ class PageFragmentLoadState(private var model: PageViewModel, return isDifferent } + fun getCurrentTab(): Tab { + return currentTab + } + fun goBack(): Boolean { if (currentTab.canGoBack()) { currentTab.moveBack() @@ -259,7 +262,7 @@ class PageFragmentLoadState(private var model: PageViewModel, fragment.requireActivity().invalidateOptionsMenu() // Update our tab list to prevent ZH variants issue. - WikipediaApp.instance.tabList.getOrNull(WikipediaApp.instance.tabCount - 1)?.setBackStackPositionTitle(title) + currentTab.setBackStackPositionTitle(title) // Update our history entry, in case the Title was changed (i.e. normalized) model.curEntry?.let { diff --git a/app/src/main/java/org/wikipedia/page/tabs/PageBackStackItem.kt b/app/src/main/java/org/wikipedia/page/tabs/PageBackStackItem.kt new file mode 100644 index 00000000000..c70d4163e87 --- /dev/null +++ b/app/src/main/java/org/wikipedia/page/tabs/PageBackStackItem.kt @@ -0,0 +1,52 @@ +package org.wikipedia.page.tabs + +import androidx.room.Entity +import androidx.room.PrimaryKey +import kotlinx.serialization.Serializable +import org.wikipedia.dataclient.WikiSite +import org.wikipedia.history.HistoryEntry +import org.wikipedia.json.PageBackStackItemSerializer +import org.wikipedia.page.PageTitle +import java.util.Date + +@Entity +@Serializable(with = PageBackStackItemSerializer::class) +class PageBackStackItem( + @PrimaryKey(autoGenerate = true) val id: Long = 0, + var apiTitle: String = "", + var displayTitle: String = "", + var langCode: String = "", + var namespace: String = "", + val timestamp: Long = Date().time, + var scrollY: Int = 0, + var source: Int = HistoryEntry.Companion.SOURCE_INTERNAL_LINK, + var thumbUrl: String? = null, + var description: String? = null, + var extract: String? = null +) { + // This constructor is used for creating a new PageBackStackItem from a PageTitle and HistoryEntry. + // The old val title and val entry were removed to avoid confusion with the new parameters. + constructor(title: PageTitle, entry: HistoryEntry) : this( + apiTitle = title.prefixedText, + displayTitle = title.displayText, + langCode = title.wikiSite.languageCode, + namespace = title.namespace, + thumbUrl = title.thumbUrl, + description = title.description, + extract = title.extract, + source = entry.source + ) + + fun getPageTitle(): PageTitle { + return PageTitle(namespace, apiTitle, WikiSite.Companion.forLanguageCode(langCode)).also { + it.displayText = displayTitle + it.thumbUrl = thumbUrl + it.description = description + it.extract = extract + } + } + + fun getHistoryEntry(): HistoryEntry { + return HistoryEntry(getPageTitle(), source, Date(timestamp)) + } +} diff --git a/app/src/main/java/org/wikipedia/page/tabs/PageBackStackItemDao.kt b/app/src/main/java/org/wikipedia/page/tabs/PageBackStackItemDao.kt new file mode 100644 index 00000000000..ecc3fb03790 --- /dev/null +++ b/app/src/main/java/org/wikipedia/page/tabs/PageBackStackItemDao.kt @@ -0,0 +1,25 @@ +package org.wikipedia.page.tabs + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query + +@Dao +interface PageBackStackItemDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertPageBackStackItem(item: PageBackStackItem): Long + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertPageBackStackItems(items: List): List + + @Query("SELECT * FROM PageBackStackItem WHERE id IN (:ids)") + suspend fun getPageBackStackItems(ids: List): List + + @Query("DELETE FROM PageBackStackItem WHERE id IN (:ids)") + suspend fun deletePageBackStackItemsById(ids: List) + + @Query("DELETE FROM PageBackStackItem") + suspend fun deleteAll() +} diff --git a/app/src/main/java/org/wikipedia/page/tabs/Tab.kt b/app/src/main/java/org/wikipedia/page/tabs/Tab.kt index 6dbc9200f64..2a800dc9e45 100644 --- a/app/src/main/java/org/wikipedia/page/tabs/Tab.kt +++ b/app/src/main/java/org/wikipedia/page/tabs/Tab.kt @@ -1,22 +1,53 @@ package org.wikipedia.page.tabs +import androidx.room.Entity +import androidx.room.Ignore +import androidx.room.PrimaryKey import kotlinx.serialization.Serializable -import org.wikipedia.page.PageBackStackItem import org.wikipedia.page.PageTitle +@Entity @Serializable -class Tab { - val backStack = mutableListOf() - +class Tab( + @PrimaryKey(autoGenerate = true) val id: Long = 0, + var order: Int = 0, + var backStackIds: String = "" +) { var backStackPosition: Int = -1 get() = if (field < 0) backStack.size - 1 else field - val backStackPositionTitle: PageTitle? - get() = if (backStack.isEmpty()) null else backStack[backStackPosition].title + // The value of backStack will be initialized from the database in TabHelper + @Ignore + var backStack = mutableListOf() + + fun setBackStackIds(ids: List) { + backStackIds = ids.joinToString(separator = ",") + } + + fun getBackStackIds(): List { + return if (backStackIds.isEmpty()) { + emptyList() + } else { + backStackIds.split(",").mapNotNull { it.toLongOrNull() } + } + } + + fun getBackStackPositionTitle(): PageTitle? { + return backStack.getOrNull(backStackPosition)?.getPageTitle() + } fun setBackStackPositionTitle(title: PageTitle) { - backStackPositionTitle?.run { - backStack[backStackPosition].title = title + getBackStackPositionTitle()?.run { + val backStackItem = backStack[backStackPosition] + backStack[backStackPosition] = backStackItem.apply { + apiTitle = title.prefixedText + displayTitle = title.displayText + langCode = title.wikiSite.languageCode + namespace = title.namespace + thumbUrl = title.thumbUrl + description = title.description + extract = title.extract + } } } @@ -25,11 +56,11 @@ class Tab { } fun canGoForward(): Boolean { - return backStackPosition < backStack.size - 1 + return backStackPosition < getBackStackIds().size - 1 } fun moveForward() { - if (backStackPosition < backStack.size - 1) { + if (backStackPosition < getBackStackIds().size - 1) { backStackPosition++ } } @@ -41,23 +72,22 @@ class Tab { } fun pushBackStackItem(item: PageBackStackItem) { - // remove all backstack items past the current position - while (backStack.size > backStackPosition + 1) { - backStack.removeAt(backStackPosition + 1) - } backStack.add(item) backStackPosition = backStack.size - 1 + setBackStackIds(backStack.map { it.id }) } fun clearBackstack() { backStack.clear() backStackPosition = -1 + setBackStackIds(emptyList()) } fun squashBackstack() { backStack.lastOrNull()?.let { backStack.clear() backStack.add(it) + setBackStackIds(listOf(it.id)) backStackPosition = 0 } } diff --git a/app/src/main/java/org/wikipedia/page/tabs/TabActivity.kt b/app/src/main/java/org/wikipedia/page/tabs/TabActivity.kt index b9009cbcec0..70e409cc1d9 100644 --- a/app/src/main/java/org/wikipedia/page/tabs/TabActivity.kt +++ b/app/src/main/java/org/wikipedia/page/tabs/TabActivity.kt @@ -8,14 +8,18 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.widget.TextView +import androidx.activity.viewModels import androidx.core.view.isVisible +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.coroutines.launch import org.wikipedia.Constants import org.wikipedia.Constants.InvokeSource import org.wikipedia.R -import org.wikipedia.WikipediaApp import org.wikipedia.activity.BaseActivity import org.wikipedia.auth.AccountUtil import org.wikipedia.databinding.ActivityTabsBinding @@ -28,6 +32,7 @@ import org.wikipedia.readinglist.AddToReadingListDialog import org.wikipedia.settings.Prefs import org.wikipedia.util.DimenUtil import org.wikipedia.util.FeedbackUtil +import org.wikipedia.util.Resource import org.wikipedia.util.ResourceUtil import org.wikipedia.util.StringUtil import org.wikipedia.util.log.L @@ -35,7 +40,8 @@ import org.wikipedia.views.WikiCardView class TabActivity : BaseActivity() { private lateinit var binding: ActivityTabsBinding - private val app: WikipediaApp = WikipediaApp.instance + + private val viewModel: TabViewModel by viewModels() private var launchedFromPageActivity = false private var cancelled = true @@ -43,11 +49,98 @@ class TabActivity : BaseActivity() { super.onCreate(savedInstanceState) binding = ActivityTabsBinding.inflate(layoutInflater) setContentView(binding.root) + binding.tabRecyclerView.adapter = TabItemAdapter() binding.tabCountsView.updateTabCount(false) binding.tabCountsView.setOnClickListener { onBackPressedDispatcher.onBackPressed() } FeedbackUtil.setButtonTooltip(binding.tabCountsView, binding.tabButtonNotifications) - binding.tabRecyclerView.adapter = TabItemAdapter() + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + launch { + viewModel.uiState.collect { + when (it) { + is Resource.Loading -> onLoading() + is Resource.Success -> onSuccess(it.data) + is Resource.Error -> onError(it.throwable) + } + } + } + + launch { + viewModel.undoTabsState.collect { + when (it) { + is Resource.Success -> { + (binding.tabRecyclerView.adapter as TabItemAdapter).setList(it.data.second) + binding.tabRecyclerView.adapter?.notifyItemInserted(it.data.first) + binding.tabRecyclerView.adapter?.notifyItemRangeChanged(0, viewModel.list.size) + binding.tabCountsView.updateTabCount(false) + } + + is Resource.Error -> { + FeedbackUtil.showError(this@TabActivity, it.throwable) + } + } + } + } + + launch { + viewModel.deleteTabsState.collect { + when (it) { + is Resource.Success -> { + binding.tabCountsView.updateTabCount(false) + (binding.tabRecyclerView.adapter as TabItemAdapter).setList(viewModel.list) + val firstIndex = it.data.first.indexOfFirst { tab -> tab.id == it.data.second.firstOrNull()?.id } + binding.tabRecyclerView.adapter?.notifyItemRangeRemoved(firstIndex, it.data.second.size) + binding.tabRecyclerView.adapter?.notifyItemRangeChanged(0, viewModel.list.size) + setResult(RESULT_LOAD_FROM_BACKSTACK) + showUndoSnackbar(firstIndex, it.data.first, it.data.second) + cancelled = false + } + + is Resource.Error -> { + FeedbackUtil.showError(this@TabActivity, it.throwable) + } + } + } + } + + launch { + viewModel.saveToListState.collect { + when (it) { + is Resource.Success -> { + ExclusiveBottomSheetPresenter.show(supportFragmentManager, + AddToReadingListDialog.newInstance(it.data, InvokeSource.TABS_ACTIVITY)) + } + + is Resource.Error -> { + FeedbackUtil.showError(this@TabActivity, it.throwable) + } + } + } + } + + launch { + viewModel.clickState.collect { + when (it) { + is Resource.Success -> { + cancelled = false + if (launchedFromPageActivity) { + setResult(RESULT_LOAD_FROM_BACKSTACK) + } else { + startActivity(PageActivity.newIntent(this@TabActivity)) + } + finish() + } + + is Resource.Error -> { + FeedbackUtil.showError(this@TabActivity, it.throwable) + } + } + } + } + } + } + val touchCallback = SwipeableTabTouchHelperCallback(this) touchCallback.swipeableEnabled = true val itemTouchHelper = ItemTouchHelper(touchCallback) @@ -69,11 +162,6 @@ class TabActivity : BaseActivity() { } } - override fun onPause() { - super.onPause() - app.commitTabState() - } - override fun onResume() { super.onResume() updateNotificationsButton(false) @@ -91,18 +179,11 @@ class TabActivity : BaseActivity() { true } R.id.menu_close_all_tabs -> { - if (app.tabList.isNotEmpty()) { + if (viewModel.list.isNotEmpty()) { MaterialAlertDialogBuilder(this).run { setMessage(R.string.close_all_tabs_confirm) setPositiveButton(R.string.close_all_tabs_confirm_yes) { _, _ -> - L.d("All tabs removed.") - val appTabs = app.tabList.toMutableList() - app.tabList.clear() - binding.tabCountsView.updateTabCount(false) - binding.tabRecyclerView.adapter?.notifyItemRangeRemoved(0, appTabs.size) - setResult(RESULT_LOAD_FROM_BACKSTACK) - showUndoAllSnackbar(appTabs) - cancelled = false + viewModel.deleteTabs(viewModel.list.toList()) } setNegativeButton(R.string.close_all_tabs_confirm_no, null) .show() @@ -111,8 +192,8 @@ class TabActivity : BaseActivity() { true } R.id.menu_save_all_tabs -> { - if (app.tabList.isNotEmpty()) { - saveTabsToList() + if (viewModel.list.isNotEmpty()) { + viewModel.saveToList() } true } @@ -128,10 +209,29 @@ class TabActivity : BaseActivity() { updateNotificationsButton(true) } - private fun saveTabsToList() { - val titlesList = app.tabList.filter { it.backStackPositionTitle != null }.map { it.backStackPositionTitle!! } - ExclusiveBottomSheetPresenter.show(supportFragmentManager, - AddToReadingListDialog.newInstance(titlesList, InvokeSource.TABS_ACTIVITY)) + private fun onLoading() { + binding.tabRecyclerView.isVisible = false + binding.errorView.isVisible = false + binding.progressBar.isVisible = true + } + + private fun onSuccess(list: List) { + binding.progressBar.isVisible = false + binding.errorView.isVisible = false + binding.tabRecyclerView.isVisible = true + (binding.tabRecyclerView.adapter as TabItemAdapter).setList(list) + binding.tabRecyclerView.adapter?.notifyItemRangeChanged(0, list.size) + binding.tabCountsView.updateTabCount(false) + } + + private fun onError(throwable: Throwable) { + L.e(throwable) + binding.progressBar.isVisible = false + binding.tabRecyclerView.isVisible = false + binding.errorView.isVisible = true + binding.errorView.backClickListener = View.OnClickListener { + finish() + } } private fun openNewTab() { @@ -144,29 +244,15 @@ class TabActivity : BaseActivity() { finish() } - private fun showUndoSnackbar(index: Int, appTab: Tab, adapterPosition: Int) { - appTab.backStackPositionTitle?.let { - FeedbackUtil.makeSnackbar(this, getString(R.string.tab_item_closed, it.displayText)).run { - setAction(R.string.reading_list_item_delete_undo) { - app.tabList.add(index, appTab) - binding.tabRecyclerView.adapter?.notifyItemInserted(adapterPosition) - binding.tabCountsView.updateTabCount(false) - if (adapterPosition == 0 && app.tabCount > 1) { - binding.tabRecyclerView.adapter?.notifyItemChanged(1) - } - } - show() - } + private fun showUndoSnackbar(undoPosition: Int, originalTabs: List, deletedTabs: List) { + val snackBarMessage = if (deletedTabs.size == 1) { + getString(R.string.tab_item_closed, deletedTabs.first().getBackStackPositionTitle()?.displayText.orEmpty()) + } else { + getString(R.string.all_tab_items_closed) } - } - - private fun showUndoAllSnackbar(appTabs: MutableList) { - FeedbackUtil.makeSnackbar(this, getString(R.string.all_tab_items_closed)).run { + FeedbackUtil.makeSnackbar(this, snackBarMessage).run { setAction(R.string.reading_list_item_delete_undo) { - app.tabList.addAll(appTabs) - appTabs.clear() - binding.tabRecyclerView.adapter?.notifyItemRangeInserted(0, app.tabList.size) - binding.tabCountsView.updateTabCount(false) + viewModel.undoDeleteTabs(undoPosition, originalTabs) } show() } @@ -197,14 +283,12 @@ class TabActivity : BaseActivity() { } } - private fun adapterPositionToTabIndex(adapterPosition: Int): Int { - return app.tabList.size - adapterPosition - 1 - } - private open inner class TabViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener, SwipeableTabTouchHelperCallback.Callback { + lateinit var tab: Tab open fun bindItem(tab: Tab, position: Int) { - itemView.findViewById(R.id.tabArticleTitle).text = StringUtil.fromHtml(tab.backStackPositionTitle?.displayText.orEmpty()) - itemView.findViewById(R.id.tabArticleDescription).text = StringUtil.fromHtml(tab.backStackPositionTitle?.description.orEmpty()) + this.tab = tab + itemView.findViewById(R.id.tabArticleTitle).text = StringUtil.fromHtml(tab.getBackStackPositionTitle()?.displayText.orEmpty()) + itemView.findViewById(R.id.tabArticleDescription).text = StringUtil.fromHtml(tab.getBackStackPositionTitle()?.description.orEmpty()) itemView.findViewById(R.id.tabContainer).setOnClickListener(this) itemView.findViewById(R.id.tabCloseButton).setOnClickListener(this) itemView.findViewById(R.id.tabCardView).run { @@ -214,23 +298,8 @@ class TabActivity : BaseActivity() { } override fun onClick(v: View) { - val adapterPosition = bindingAdapterPosition - val index = adapterPositionToTabIndex(adapterPosition) - if (index < 0 || index >= app.tabList.size) { - return - } if (v.id == R.id.tabContainer) { - if (index < app.tabList.size - 1) { - val tab = app.tabList.removeAt(index) - app.tabList.add(tab) - } - cancelled = false - if (launchedFromPageActivity) { - setResult(RESULT_LOAD_FROM_BACKSTACK) - } else { - startActivity(PageActivity.newIntent(this@TabActivity)) - } - finish() + viewModel.moveTabToForeground(tab) } else if (v.id == R.id.tabCloseButton) { doCloseTab() } @@ -245,30 +314,28 @@ class TabActivity : BaseActivity() { } private fun doCloseTab() { - val adapterPosition = bindingAdapterPosition - val index = adapterPositionToTabIndex(adapterPosition) - val appTab = app.tabList.removeAt(index) - binding.tabCountsView.updateTabCount(false) - bindingAdapter?.notifyItemRemoved(adapterPosition) - if (adapterPosition == 0) { - bindingAdapter?.notifyItemChanged(0) - } - setResult(RESULT_LOAD_FROM_BACKSTACK) - showUndoSnackbar(index, appTab, adapterPosition) + viewModel.deleteTabs(listOf(tab)) } } - private inner class TabItemAdapter : RecyclerView.Adapter() { + private inner class TabItemAdapter() : RecyclerView.Adapter() { + private var list: List = emptyList() + + fun setList(newList: List) { + list = newList + } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TabViewHolder { - return TabViewHolder(layoutInflater.inflate(R.layout.item_tab_contents, parent, false)) + val itemView = layoutInflater.inflate(R.layout.item_tab_contents, parent, false) + return TabViewHolder(itemView) } override fun getItemCount(): Int { - return app.tabList.size + return list.size } override fun onBindViewHolder(holder: TabViewHolder, position: Int) { - holder.bindItem(app.tabList[adapterPositionToTabIndex(position)], position) + holder.bindItem(list[position], position) } } diff --git a/app/src/main/java/org/wikipedia/page/tabs/TabDao.kt b/app/src/main/java/org/wikipedia/page/tabs/TabDao.kt new file mode 100644 index 00000000000..ec87fffe083 --- /dev/null +++ b/app/src/main/java/org/wikipedia/page/tabs/TabDao.kt @@ -0,0 +1,41 @@ +package org.wikipedia.page.tabs + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update + +@Dao +interface TabDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertTabs(tabs: List) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertTab(tab: Tab) + + @Query("SELECT * FROM Tab ORDER BY `order` ASC") + suspend fun getTabs(): List + + @Query("SELECT * FROM Tab WHERE id = :id") + suspend fun getTabById(id: Long): Tab? + + @Query("SELECT * FROM Tab ORDER BY `order` ASC LIMIT 1") + suspend fun getForegroundTab(): Tab? + + @Query("DELETE FROM Tab") + suspend fun deleteAll() + + @Delete + suspend fun deleteTabs(tabs: List) + + @Update + suspend fun updateTab(tab: Tab) + + @Update + suspend fun updateTabs(tabs: List) + + @Delete + suspend fun deleteTab(tab: Tab) +} diff --git a/app/src/main/java/org/wikipedia/page/tabs/TabHelper.kt b/app/src/main/java/org/wikipedia/page/tabs/TabHelper.kt new file mode 100644 index 00000000000..fc005e9a2b9 --- /dev/null +++ b/app/src/main/java/org/wikipedia/page/tabs/TabHelper.kt @@ -0,0 +1,222 @@ +package org.wikipedia.page.tabs + +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.wikipedia.database.AppDatabase +import org.wikipedia.history.HistoryEntry +import org.wikipedia.page.PageTitle +import org.wikipedia.util.log.L + +object TabHelper { + + const val MAX_TABS = 100 + + val coroutineScope = CoroutineScope(Dispatchers.IO) + val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable -> + L.e(throwable) + } + + var count: Int = 0 + + init { + coroutineScope.launch { + updateTabCount() + } + } + + suspend fun updateTabCount() { + val tabs = AppDatabase.instance.tabDao().getTabs().filter { it.getBackStackIds().isNotEmpty() } + count = tabs.size + } + + suspend fun getCurrentTab(): Tab { + return withContext(Dispatchers.IO) { + val foregroundTab = AppDatabase.instance.tabDao().getForegroundTab() + if (foregroundTab == null) { + return@withContext Tab() + } + // Use the backStackIds to get the full backStack items from the database + val backStackItems = AppDatabase.instance.pageBackStackItemDao() + .getPageBackStackItems(foregroundTab.getBackStackIds()) + foregroundTab.backStack = backStackItems.toMutableList() + foregroundTab + } + } + + fun removeTab(tab: Tab) { + coroutineScope.launch(coroutineExceptionHandler) { + deleteTabs(listOf(tab)) + } + } + + fun findTabByTitle(title: PageTitle): Tab? { + // TODO: handle this with coroutines if we have viewModel + return runBlocking { + val tabs = AppDatabase.instance.tabDao().getTabs() + tabs.firstOrNull { tab -> + val backStackItems = AppDatabase.instance.pageBackStackItemDao() + .getPageBackStackItems(tab.getBackStackIds()) + backStackItems.any { it.getPageTitle() == title } + } + } + } + + suspend fun createNewTab(entry: HistoryEntry): Tab { + return withContext(Dispatchers.IO) { + val tab = Tab() + // Add a new PageBackStackItem to database + val pageBackStackItem = PageBackStackItem( + apiTitle = entry.title.prefixedText, + displayTitle = entry.title.displayText, + langCode = entry.title.wikiSite.languageCode, + namespace = entry.title.namespace, + thumbUrl = entry.title.thumbUrl, + description = entry.title.description, + extract = entry.title.extract, + source = entry.source + ) + tab.backStack.add(pageBackStackItem) + tab.setBackStackIds( + listOf( + AppDatabase.instance.pageBackStackItemDao() + .insertPageBackStackItem(pageBackStackItem) + ) + ) + tab + } + } + + suspend fun trimTabCount() { + withContext(Dispatchers.IO) { + val tabs = AppDatabase.instance.tabDao().getTabs().filter { it.getBackStackIds().isNotEmpty() } + if (tabs.size > MAX_TABS) { + // Sort tabs by order and remove the oldest ones + val sortedTabs = tabs.sortedBy { it.order } + val tabsToDelete = sortedTabs.take(sortedTabs.size - MAX_TABS) + deleteTabs(tabsToDelete) + } + } + } + + fun openInNewBackgroundTab(coroutineScope: CoroutineScope = TabHelper.coroutineScope, entry: HistoryEntry, action: () -> Unit = {}) { + coroutineScope.launch(coroutineExceptionHandler) { + val tab = Tab() + // Add a new PageBackStackItem to database + val pageBackStackItem = PageBackStackItem( + apiTitle = entry.title.prefixedText, + displayTitle = entry.title.displayText, + langCode = entry.title.wikiSite.languageCode, + namespace = entry.title.namespace, + thumbUrl = entry.title.thumbUrl, + description = entry.title.description, + extract = entry.title.extract, + source = entry.source + ) + tab.backStack.add(pageBackStackItem) + insertTabs(listOf(tab)) + action() + } + } + + suspend fun insertTabs(tabs: List, toForeground: Boolean = false) { + withContext(Dispatchers.IO) { + if (tabs.isEmpty()) return@withContext + val allTabs = AppDatabase.instance.tabDao().getTabs() + // get the last order from the table + var lastOrder = if (toForeground) 0 else allTabs.maxOfOrNull { it.order } ?: 0 + tabs.forEach { tab -> + val ids = AppDatabase.instance.pageBackStackItemDao().insertPageBackStackItems(tab.backStack) + tab.setBackStackIds(ids) + if (tab.order == 0) { + // If the order is not set, assign a new order + tab.order = ++lastOrder + } + } + var finalTabs = tabs + if (toForeground) { + // Re-arrange the existing tabs' order + allTabs.forEachIndexed { index, existingTab -> + existingTab.order = ++lastOrder + } + finalTabs = finalTabs + allTabs + } + + AppDatabase.instance.tabDao().insertTabs(finalTabs) + updateTabCount() + } + } + + suspend fun deleteTabs(tabs: List) { + withContext(Dispatchers.IO) { + if (tabs.isEmpty()) return@withContext + val backStackIdsToDelete = tabs.flatMap { it.getBackStackIds() }.distinct() + // Delete all backStack items associated with the tabs to be deleted + AppDatabase.instance.pageBackStackItemDao().deletePageBackStackItemsById(backStackIdsToDelete) + AppDatabase.instance.tabDao().deleteTabs(tabs) + // Reset the order of the remaining tabs + val remainingTabs = AppDatabase.instance.tabDao().getTabs() + remainingTabs.forEachIndexed { index, tab -> + tab.order = index + 1 + } + AppDatabase.instance.tabDao().updateTabs(remainingTabs) + updateTabCount() + } + } + + suspend fun updateTab(tab: Tab) { + withContext(Dispatchers.IO) { + // Find the existing tab in the database + val existingTab = AppDatabase.instance.tabDao().getTabById(tab.id) + if (existingTab != null) { + L.d("onPageLoadComplete existingTab " + existingTab.id) + // First, find removed backstack items + val removedBackStacks = + existingTab.getBackStackIds().subtract(tab.getBackStackIds()).filter { it != 0L } + if (removedBackStacks.isNotEmpty()) { + L.d("onPageLoadComplete removedBackStacks") + // Delete removed backstack items from the database + AppDatabase.instance.pageBackStackItemDao() + .deletePageBackStackItemsById(removedBackStacks.toList()) + } + + // Second, find new backstack items if the id is -1, insert them to the database + val backStackIds = mutableListOf() + tab.backStack.forEach { item -> + var backStackId = item.id + L.d("onPageLoadComplete backStack $backStackId") + if (item.id == 0L) { + val newId = AppDatabase.instance.pageBackStackItemDao() + .insertPageBackStackItem(item) + backStackId = newId + } + backStackIds.add(backStackId) + } + + // Finally, update the tab with the new backStackIds and order + L.d("onPageLoadComplete $backStackIds") + tab.setBackStackIds(backStackIds) + AppDatabase.instance.tabDao().updateTab(tab) + updateTabCount() + } + } + } + + suspend fun moveTabToForeground(tab: Tab): List { + return withContext(Dispatchers.IO) { + val list = AppDatabase.instance.tabDao().getTabs().toMutableList() + // Change the tab's order to 1 and update the rest of the tabs + list.removeIf { it.id == tab.id } + list.add(0, tab) + list.forEachIndexed { index, t -> + t.order = index + } + AppDatabase.instance.tabDao().updateTabs(list) + updateTabCount() + return@withContext list + } + } +} diff --git a/app/src/main/java/org/wikipedia/page/tabs/TabViewModel.kt b/app/src/main/java/org/wikipedia/page/tabs/TabViewModel.kt new file mode 100644 index 00000000000..cca7f213c90 --- /dev/null +++ b/app/src/main/java/org/wikipedia/page/tabs/TabViewModel.kt @@ -0,0 +1,106 @@ +package org.wikipedia.page.tabs + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.wikipedia.database.AppDatabase +import org.wikipedia.page.PageTitle +import org.wikipedia.util.Resource + +class TabViewModel : ViewModel() { + + private val _saveToListState = MutableStateFlow(Resource>()) + val saveToListState = _saveToListState.asStateFlow() + + private val _deleteTabsState = MutableStateFlow(Resource, List>>()) + val deleteTabsState = _deleteTabsState.asStateFlow() + + private val _undoTabsState = MutableStateFlow(Resource>>()) + val undoTabsState = _undoTabsState.asStateFlow() + + private val _uiState = MutableStateFlow(Resource>()) + val uiState = _uiState.asStateFlow() + + private val _clickState = MutableStateFlow(Resource()) + val clickState = _clickState.asStateFlow() + + var list = mutableListOf() + + init { + setup() + } + + private fun setup() { + viewModelScope.launch(CoroutineExceptionHandler { _, throwable -> + _uiState.value = Resource.Error(throwable) + }) { + _uiState.value = Resource.Loading() + val tabs = fetchTabs() + _uiState.value = Resource.Success(tabs) + } + } + + private suspend fun fetchTabs(): List { + return withContext(Dispatchers.IO) { + val tabs = AppDatabase.instance.tabDao().getTabs() + tabs.forEach { tab -> + // Use the backStackIds to get the full backStack items from the database + val backStackItems = AppDatabase.instance.pageBackStackItemDao() + .getPageBackStackItems(tab.getBackStackIds()) + tab.backStack = backStackItems.toMutableList() + } + list = tabs.toMutableList() + tabs + } + } + + fun saveToList() { + viewModelScope.launch(CoroutineExceptionHandler { _, throwable -> + _saveToListState.value = Resource.Error(throwable) + }) { + _saveToListState.value = Resource.Loading() + val pageTitles = list.mapNotNull { it.getBackStackPositionTitle() } + if (pageTitles.isNotEmpty()) { + _saveToListState.value = Resource.Success(pageTitles) + } else { + _saveToListState.value = Resource.Error(Exception("Empty list")) + } + } + } + + fun deleteTabs(deletedTabs: List) { + viewModelScope.launch(CoroutineExceptionHandler { _, throwable -> + _deleteTabsState.value = Resource.Error(throwable) + }) { + _deleteTabsState.value = Resource.Loading() + val originalList = list.toList() + TabHelper.deleteTabs(deletedTabs) + list.removeAll(deletedTabs) + _deleteTabsState.value = Resource.Success(originalList to deletedTabs) + } + } + + fun undoDeleteTabs(undoPosition: Int, tabs: List) { + viewModelScope.launch(CoroutineExceptionHandler { _, throwable -> + _undoTabsState.value = Resource.Error(throwable) + }) { + TabHelper.insertTabs(tabs) + val tabs = fetchTabs() + _undoTabsState.value = Resource.Success(undoPosition to tabs) + } + } + + fun moveTabToForeground(tab: Tab) { + viewModelScope.launch(CoroutineExceptionHandler { _, throwable -> + _clickState.value = Resource.Error(throwable) + }) { + TabHelper.moveTabToForeground(tab) + _clickState.value = Resource.Success(true) + } + } +} diff --git a/app/src/main/java/org/wikipedia/places/PlacesFragment.kt b/app/src/main/java/org/wikipedia/places/PlacesFragment.kt index 30d3725d054..0e1ef9315c2 100644 --- a/app/src/main/java/org/wikipedia/places/PlacesFragment.kt +++ b/app/src/main/java/org/wikipedia/places/PlacesFragment.kt @@ -33,6 +33,7 @@ import androidx.core.view.WindowInsetsCompat import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import org.maplibre.android.MapLibre @@ -79,6 +80,7 @@ import org.wikipedia.page.PageActivity import org.wikipedia.page.PageTitle import org.wikipedia.page.linkpreview.LinkPreviewDialog import org.wikipedia.page.tabs.TabActivity +import org.wikipedia.page.tabs.TabHelper import org.wikipedia.readinglist.LongPressMenu import org.wikipedia.readinglist.ReadingListBehaviorsUtil import org.wikipedia.readinglist.database.ReadingListPage @@ -91,7 +93,6 @@ import org.wikipedia.util.GeoUtil import org.wikipedia.util.Resource import org.wikipedia.util.ResourceUtil import org.wikipedia.util.StringUtil -import org.wikipedia.util.TabUtil import org.wikipedia.util.log.L import org.wikipedia.views.DrawableItemDecoration import org.wikipedia.views.ViewUtil @@ -217,7 +218,7 @@ class PlacesFragment : Fragment(), LinkPreviewDialog.LoadPageCallback, LinkPrevi binding.tabsButton.setOnClickListener { PlacesEvent.logAction("tabs_view_click", "search_bar_view") - if (WikipediaApp.instance.tabCount == 1) { + if (TabHelper.count == 1) { startActivity(PageActivity.newIntent(requireActivity())) } else { startActivity(TabActivity.newIntent(requireActivity())) @@ -477,7 +478,7 @@ class PlacesFragment : Fragment(), LinkPreviewDialog.LoadPageCallback, LinkPrevi } private fun updateSearchCardViews() { - val tabsCount = WikipediaApp.instance.tabCount + val tabsCount = TabHelper.count binding.tabsButton.isVisible = tabsCount != 0 binding.tabsButton.updateTabCount(false) @@ -741,9 +742,9 @@ class PlacesFragment : Fragment(), LinkPreviewDialog.LoadPageCallback, LinkPrevi override fun onLinkPreviewLoadPage(title: PageTitle, entry: HistoryEntry, inNewTab: Boolean) { if (inNewTab) { - TabUtil.openInNewBackgroundTab(entry) + TabHelper.openInNewBackgroundTab(viewLifecycleOwner.lifecycleScope, entry) requireActivity().invalidateOptionsMenu() - binding.tabsButton.isVisible = WikipediaApp.instance.tabCount > 0 + binding.tabsButton.isVisible = TabHelper.count > 0 binding.tabsButton.updateTabCount(true) } else { startActivity(PageActivity.newIntentForNewTab(requireActivity(), entry, entry.title)) diff --git a/app/src/main/java/org/wikipedia/search/SearchResultsViewModel.kt b/app/src/main/java/org/wikipedia/search/SearchResultsViewModel.kt index 53d78185125..83ce6c97c78 100644 --- a/app/src/main/java/org/wikipedia/search/SearchResultsViewModel.kt +++ b/app/src/main/java/org/wikipedia/search/SearchResultsViewModel.kt @@ -125,15 +125,25 @@ class SearchResultsViewModel : ViewModel() { return null } - private fun getSearchResultsFromTabs(wikiSite: WikiSite, searchTerm: String): SearchResults { - WikipediaApp.instance.tabList.forEach { tab -> - tab.backStackPositionTitle?.let { - if (wikiSite == it.wikiSite && StringUtil.fromHtml(it.displayText).contains(searchTerm, true)) { - return SearchResults(mutableListOf(SearchResult(it, SearchResult.SearchResultType.TAB_LIST))) - } + private suspend fun getSearchResultsFromTabs(wikiSite: WikiSite, searchTerm: String): SearchResults { + return withContext(Dispatchers.IO) { + val tabs = AppDatabase.instance.tabDao().getTabs() + tabs.forEach { tab -> + // Use the backStackIds to get the full backStack items from the database + val backStackItems = AppDatabase.instance.pageBackStackItemDao() + .getPageBackStackItems(tab.getBackStackIds()) + tab.backStack = backStackItems.toMutableList() + } + return@withContext tabs.firstOrNull { + it.getBackStackPositionTitle()?.let { title -> + title.wikiSite == wikiSite && StringUtil.fromHtml(title.displayText).contains(searchTerm, true) + } ?: false + }?.let { tab -> + SearchResults(mutableListOf(SearchResult(tab.getBackStackPositionTitle()!!, SearchResult.SearchResultType.TAB_LIST))) + } ?: run { + SearchResults() } } - return SearchResults() } } } diff --git a/app/src/main/java/org/wikipedia/settings/Prefs.kt b/app/src/main/java/org/wikipedia/settings/Prefs.kt index 072891986c8..d6bff0cd3aa 100644 --- a/app/src/main/java/org/wikipedia/settings/Prefs.kt +++ b/app/src/main/java/org/wikipedia/settings/Prefs.kt @@ -93,13 +93,13 @@ object Prefs { get() = PrefsIoUtil.getString(R.string.preference_key_remote_config, "").orEmpty().ifEmpty { "{}" } set(json) = PrefsIoUtil.setString(R.string.preference_key_remote_config, json) + // TODO: remove on 2026-02-01 var tabs get() = JsonUtil.decodeFromString>(PrefsIoUtil.getString(R.string.preference_key_tabs, null)) ?: emptyList() set(tabs) = PrefsIoUtil.setString(R.string.preference_key_tabs, JsonUtil.encodeToString(tabs)) - val hasTabs get() = PrefsIoUtil.contains(R.string.preference_key_tabs) - + // TODO: remove on 2026-08-01 fun clearTabs() { PrefsIoUtil.remove(R.string.preference_key_tabs) } diff --git a/app/src/main/java/org/wikipedia/util/TabUtil.kt b/app/src/main/java/org/wikipedia/util/TabUtil.kt deleted file mode 100644 index 12660e017d2..00000000000 --- a/app/src/main/java/org/wikipedia/util/TabUtil.kt +++ /dev/null @@ -1,23 +0,0 @@ -package org.wikipedia.util - -import org.wikipedia.Constants -import org.wikipedia.WikipediaApp -import org.wikipedia.history.HistoryEntry -import org.wikipedia.page.PageBackStackItem -import org.wikipedia.page.tabs.Tab - -object TabUtil { - - fun openInNewBackgroundTab(entry: HistoryEntry) { - val app = WikipediaApp.instance - val tab = if (app.tabCount == 0) app.tabList[0] else Tab() - if (app.tabCount > 0) { - app.tabList.add(0, tab) - while (app.tabList.size > Constants.MAX_TABS) { - app.tabList.removeAt(0) - } - } - tab.backStack.add(PageBackStackItem(entry.title, entry)) - app.commitTabState() - } -} diff --git a/app/src/main/java/org/wikipedia/views/TabCountsView.kt b/app/src/main/java/org/wikipedia/views/TabCountsView.kt index 655c208d727..2bcb022713a 100644 --- a/app/src/main/java/org/wikipedia/views/TabCountsView.kt +++ b/app/src/main/java/org/wikipedia/views/TabCountsView.kt @@ -8,8 +8,9 @@ import android.view.ViewGroup import android.view.animation.AnimationUtils import android.widget.FrameLayout import androidx.core.widget.TextViewCompat +import kotlinx.coroutines.runBlocking import org.wikipedia.R -import org.wikipedia.WikipediaApp +import org.wikipedia.database.AppDatabase import org.wikipedia.databinding.ViewTabsCountBinding import org.wikipedia.util.DimenUtil import org.wikipedia.util.ResourceUtil @@ -25,11 +26,14 @@ class TabCountsView(context: Context, attrs: AttributeSet? = null) : FrameLayout } fun updateTabCount(animation: Boolean) { - val count = WikipediaApp.instance.tabCount - binding.tabsCountText.text = count.toString() - TextViewCompat.setAutoSizeTextTypeUniformWithConfiguration(binding.tabsCountText, 7, 10, 1, TypedValue.COMPLEX_UNIT_SP) - if (animation) { - startAnimation(AnimationUtils.loadAnimation(context, R.anim.tab_list_zoom_enter)) + // TODO: determine if this should be run on the main thread or not + runBlocking { + val tabs = AppDatabase.instance.tabDao().getTabs().filter { it.getBackStackIds().isNotEmpty() } + binding.tabsCountText.text = tabs.size.toString() + TextViewCompat.setAutoSizeTextTypeUniformWithConfiguration(binding.tabsCountText, 7, 10, 1, TypedValue.COMPLEX_UNIT_SP) + if (animation) { + startAnimation(AnimationUtils.loadAnimation(context, R.anim.tab_list_zoom_enter)) + } } } } diff --git a/app/src/main/res/layout/activity_tabs.xml b/app/src/main/res/layout/activity_tabs.xml index d3bd4083579..693429bba29 100644 --- a/app/src/main/res/layout/activity_tabs.xml +++ b/app/src/main/res/layout/activity_tabs.xml @@ -44,4 +44,17 @@ + + + +