Migrate database to new version

* Rename some Account fields
* Migrate Account.type enum field from INTEGER to TEXT
This commit is contained in:
Shinokuni 2024-10-08 17:59:32 +02:00
parent fb68f1f492
commit 4753454a9d
30 changed files with 646 additions and 95 deletions

View File

@ -12,9 +12,9 @@ abstract class Credentials(val authorization: String?, val url: String) {
companion object {
fun toCredentials(account: Account): Credentials {
val endPoint = getEndPoint(account.accountType!!)
val endPoint = getEndPoint(account.type!!)
return when (account.accountType) {
return when (account.type) {
AccountType.NEXTCLOUD_NEWS -> NextcloudNewsCredentials(account.login, account.password, account.url + endPoint)
AccountType.FRESHRSS -> FreshRSSCredentials(account.token, account.url + endPoint)
AccountType.FEVER -> FeverCredentials(account.login, account.password, account.url + endPoint)

View File

@ -22,7 +22,7 @@ class GetFoldersWithFeedsTest {
private lateinit var database: Database
private lateinit var getFoldersWithFeeds: GetFoldersWithFeeds
private val account = Account(accountType = AccountType.LOCAL)
private val account = Account(type = AccountType.LOCAL)
@Before
fun before() {

View File

@ -32,7 +32,7 @@ import kotlin.test.assertTrue
class LocalRSSRepositoryTest : KoinTest {
private val mockServer: MockWebServer = MockWebServer()
private val account = Account(accountType = AccountType.LOCAL)
private val account = Account(type = AccountType.LOCAL)
private lateinit var database: Database
private lateinit var repository: LocalRSSRepository
private lateinit var feeds: List<Feed>

View File

@ -32,20 +32,20 @@ class SyncAnalyzerTest {
NullPointerException("Notification content shouldn't be null")
private val account1 = Account(
accountName = "test account 1",
accountType = AccountType.FRESHRSS,
name = "test account 1",
type = AccountType.FRESHRSS,
isNotificationsEnabled = true
)
private val account2 = Account(
accountName = "test account 2",
accountType = AccountType.NEXTCLOUD_NEWS,
name = "test account 2",
type = AccountType.NEXTCLOUD_NEWS,
isNotificationsEnabled = false
)
private val account3 = Account(
accountName = "test account 3",
accountType = AccountType.LOCAL,
name = "test account 3",
type = AccountType.LOCAL,
isNotificationsEnabled = true
)
@ -124,7 +124,7 @@ class SyncAnalyzerTest {
syncAnalyzer.getNotificationContent(mapOf(account1 to syncResult))?.let { content ->
assertEquals(context.getString(R.string.new_items, 2), content.text)
assertEquals(account1.accountName, content.title)
assertEquals(account1.name, content.title)
assertTrue(content.largeIcon != null)
assertTrue(content.accountId > 0)
} ?: throw nullContentException

View File

@ -59,7 +59,7 @@ val appModule = module {
single { GetFoldersWithFeeds(get()) }
factory<BaseRepository> { (account: Account) ->
when (account.accountType) {
when (account.type) {
AccountType.LOCAL -> LocalRSSRepository(get(), get(), account)
AccountType.FRESHRSS -> FreshRSSRepository(
database = get(),

View File

@ -178,8 +178,8 @@ class AccountScreenModel(
fun createLocalAccount() {
val context = get<Context>()
val account = Account(
accountName = context.getString(AccountType.LOCAL.typeName),
accountType = AccountType.LOCAL,
name = context.getString(AccountType.LOCAL.nameRes),
type = AccountType.LOCAL,
isCurrentAccount = true
)
@ -212,7 +212,7 @@ class AccountScreenModel(
@Stable
data class AccountState(
val account: Account = Account(accountName = "account", accountType = AccountType.LOCAL),
val account: Account = Account(name = "account", type = AccountType.LOCAL),
val dialog: DialogState? = null,
val synchronizationErrors: ErrorResult? = null,
val error: Exception? = null,

View File

@ -192,7 +192,7 @@ object AccountTab : Tab {
modifier = Modifier.weight(1f)
) {
Image(
painter = adaptiveIconPainterResource(id = state.account.accountType!!.iconRes),
painter = adaptiveIconPainterResource(id = state.account.type!!.iconRes),
contentDescription = null,
modifier = Modifier.size(48.dp)
)
@ -201,7 +201,7 @@ object AccountTab : Tab {
Column {
Text(
text = state.account.accountName!!,
text = state.account.name!!,
style = MaterialTheme.typography.titleLarge,
maxLines = 1,
overflow = TextOverflow.Ellipsis
@ -224,7 +224,7 @@ object AccountTab : Tab {
ThreeDotsMenu(
items = mapOf(1 to stringResource(id = R.string.rename_account)),
onItemClick = {
screenModel.openDialog(DialogState.RenameAccount(state.account.accountName!!))
screenModel.openDialog(DialogState.RenameAccount(state.account.name!!))
},
)
}
@ -303,8 +303,8 @@ object AccountTab : Tab {
for (account in state.accounts) {
SelectableImageText(
image = adaptiveIconPainterResource(id = account.accountType!!.iconRes),
text = account.accountName!!,
image = adaptiveIconPainterResource(id = account.type!!.iconRes),
text = account.name!!,
style = MaterialTheme.typography.titleMedium,
padding = MaterialTheme.spacing.mediumSpacing,
spacing = MaterialTheme.spacing.mediumSpacing,
@ -358,8 +358,8 @@ object AccountTab : Tab {
screenModel.createLocalAccount()
} else {
val account = Account(
accountType = accountType,
accountName = context.resources.getString(accountType.typeName)
type = accountType,
name = context.resources.getString(accountType.nameRes)
)
navigator.push(
AccountCredentialsScreen(

View File

@ -117,7 +117,7 @@ class AccountCredentialsScreen(
.verticalScroll(rememberScrollState())
) {
Image(
painter = adaptiveIconPainterResource(id = account.accountType!!.iconRes),
painter = adaptiveIconPainterResource(id = account.type!!.iconRes),
contentDescription = null,
modifier = Modifier.size(48.dp)
)
@ -125,7 +125,7 @@ class AccountCredentialsScreen(
ShortSpacer()
Text(
text = stringResource(id = account.accountType!!.typeName),
text = stringResource(id = account.type!!.nameRes),
style = MaterialTheme.typography.headlineMedium
)
@ -155,7 +155,7 @@ class AccountCredentialsScreen(
state.urlError != null -> {
Text(text = state.urlError!!.errorText())
}
account.accountType == AccountType.FEVER -> {
account.type == AccountType.FEVER -> {
Text(text = stringResource(R.string.provide_full_url))
}
else -> {
@ -211,7 +211,7 @@ class AccountCredentialsScreen(
state.passwordError != null -> {
Text(text = state.passwordError!!.errorText())
}
account.accountType == AccountType.FRESHRSS -> {
account.type == AccountType.FRESHRSS -> {
Text(text = stringResource(id = R.string.password_helper))
}
}

View File

@ -28,14 +28,14 @@ class AccountCredentialsScreenModel(
if (mode == AccountCredentialsScreenMode.EDIT_CREDENTIALS) {
mutableState.update {
it.copy(
name = account.accountName!!,
name = account.name!!,
url = account.url!!,
login = account.login!!,
password = account.password!!
)
}
} else {
mutableState.update { it.copy(name = account.accountName!!) }
mutableState.update { it.copy(name = account.name!!) }
}
}
@ -61,10 +61,10 @@ class AccountCredentialsScreenModel(
val newAccount = account.copy(
url = normalizedUrl,
accountName = name,
name = name,
login = login,
password = password,
accountType = account.accountType,
type = account.type,
isCurrentAccount = true
)

View File

@ -22,11 +22,10 @@ fun AccountSelectionDialog(
onDismiss = onDismiss
) {
AccountType.entries
.filter { it != AccountType.FEEDLY }
.forEach { type ->
SelectableImageText(
image = adaptiveIconPainterResource(id = type.iconRes),
text = stringResource(id = type.typeName),
text = stringResource(id = type.nameRes),
style = MaterialTheme.typography.titleMedium,
spacing = MaterialTheme.spacing.mediumSpacing,
padding = MaterialTheme.spacing.shortSpacing,

View File

@ -95,8 +95,8 @@ class AccountSelectionScreen : AndroidScreen() {
val accountType =
(state.navState as NavState.GoToAccountCredentialsScreen).accountType
val account = Account(
accountType = accountType,
accountName = stringResource(id = accountType.typeName)
type = accountType,
name = stringResource(id = accountType.nameRes)
)
navigator.push(
@ -159,7 +159,7 @@ class AccountSelectionScreen : AndroidScreen() {
SelectableImageText(
image = adaptiveIconPainterResource(id = R.mipmap.ic_launcher),
text = stringResource(id = AccountType.LOCAL.typeName),
text = stringResource(id = AccountType.LOCAL.nameRes),
style = MaterialTheme.typography.bodyLarge,
spacing = MaterialTheme.spacing.mediumSpacing,
padding = MaterialTheme.spacing.mediumSpacing,
@ -187,11 +187,10 @@ class AccountSelectionScreen : AndroidScreen() {
AccountType.entries
.filter { it != AccountType.LOCAL }
.filter { it != AccountType.FEEDLY }
.forEach { accountType ->
SelectableImageText(
image = adaptiveIconPainterResource(id = accountType.iconRes),
text = stringResource(id = accountType.typeName),
text = stringResource(id = accountType.nameRes),
style = MaterialTheme.typography.bodyLarge,
imageSize = 24.dp,
spacing = MaterialTheme.spacing.mediumSpacing,

View File

@ -55,8 +55,8 @@ class AccountSelectionScreenModel(
val context = get<Context>()
val account = Account(
url = null,
accountName = context.getString(AccountType.LOCAL.typeName),
accountType = AccountType.LOCAL,
name = context.getString(AccountType.LOCAL.nameRes),
type = AccountType.LOCAL,
isCurrentAccount = true
)

View File

@ -41,7 +41,7 @@ sealed class FolderAndFeedsState {
data class AddFeedDialogState(
val url: String = "",
val selectedAccount: Account = Account(accountName = ""),
val selectedAccount: Account = Account(name = ""),
val accounts: List<Account> = listOf(),
val error: TextFieldError? = null,
val exception: Exception? = null,

View File

@ -79,14 +79,14 @@ fun AddFeedDialog(
) {
for (account in state.accounts) {
DropdownMenuItem(
text = { Text(text = account.accountName!!) },
text = { Text(text = account.name!!) },
onClick = {
onAccountClick(account)
},
leadingIcon = {
Image(
painter = adaptiveIconPainterResource(
id = account.accountType!!.iconRes
id = account.type!!.iconRes
),
contentDescription = null,
modifier = Modifier.size(24.dp)
@ -97,7 +97,7 @@ fun AddFeedDialog(
}
OutlinedTextField(
value = state.selectedAccount.accountName!!,
value = state.selectedAccount.name!!,
readOnly = true,
onValueChange = {},
trailingIcon = {
@ -106,7 +106,7 @@ fun AddFeedDialog(
leadingIcon = {
Image(
painter = adaptiveIconPainterResource(
id = state.selectedAccount.accountType!!.iconRes
id = state.selectedAccount.type!!.iconRes
),
contentDescription = null,
modifier = Modifier.size(24.dp)

View File

@ -52,7 +52,7 @@ class ItemScreenModel(
.flatMapLatest { account ->
this@ItemScreenModel.account = account!!
if (account.accountType == AccountType.FEVER) {
if (account.type == AccountType.FEVER) {
get<SharedPreferences>().apply {
account.login = getString(account.loginKey, null)
account.password = getString(account.passwordKey, null)

View File

@ -25,7 +25,7 @@ class FeverRepository(
val authenticated = feverDataSource.login(account.login!!, account.password!!)
if (authenticated) {
account.displayedName = account.accountType!!.name
account.displayedName = account.type!!.name
} else {
throw LoginFailedException()
}

View File

@ -115,7 +115,7 @@ class LocalRSSRepository(
private suspend fun insertFeed(feed: Feed): Feed {
// TODO better handle this case
require(!database.feedDao().feedExists(feed.url!!, account.id)) {
"Feed already exists for account ${account.accountName}"
"Feed already exists for account ${account.name}"
}
return feed.apply {

View File

@ -67,11 +67,11 @@ class SyncAnalyzer(
// multiple new items from several feeds
feedsIdsForNewItems.size > 1 && itemCount > 1 -> {
NotificationContent(
title = account.accountName!!,
title = account.name!!,
text = context.getString(R.string.new_items, itemCount.toString()),
largeIcon = ContextCompat.getDrawable(
context,
account.accountType!!.iconRes
account.type!!.iconRes
)!!.toBitmap(),
accountId = account.id
)

View File

@ -131,7 +131,7 @@ class SyncWorker(
notificationBuilder.setContentTitle(
applicationContext.resources.getString(
R.string.updating_account,
account.accountName
account.name
)
)
@ -212,7 +212,7 @@ class SyncWorker(
if (result.second.isNotEmpty()) {
Log.e(
TAG,
"refreshing local account ${account.accountName}: ${result.second.size} errors"
"refreshing local account ${account.name}: ${result.second.size} errors"
)
}

View File

@ -0,0 +1,526 @@
{
"formatVersion": 1,
"database": {
"version": 5,
"identityHash": "0bac941f8b1b6003c35a6d0cdc1f2e13",
"entities": [
{
"tableName": "Feed",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `description` TEXT, `url` TEXT, `siteUrl` TEXT, `last_updated` TEXT, `color` INTEGER NOT NULL, `icon_url` TEXT, `etag` TEXT, `last_modified` TEXT, `folder_id` INTEGER, `remote_id` TEXT, `account_id` INTEGER NOT NULL, `notification_enabled` INTEGER NOT NULL DEFAULT 1, FOREIGN KEY(`folder_id`) REFERENCES `Folder`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`account_id`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "siteUrl",
"columnName": "siteUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastUpdated",
"columnName": "last_updated",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "color",
"columnName": "color",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "iconUrl",
"columnName": "icon_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "etag",
"columnName": "etag",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastModified",
"columnName": "last_modified",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "folderId",
"columnName": "folder_id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "remoteId",
"columnName": "remote_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "accountId",
"columnName": "account_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isNotificationEnabled",
"columnName": "notification_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_Feed_folder_id",
"unique": false,
"columnNames": [
"folder_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_Feed_folder_id` ON `${TABLE_NAME}` (`folder_id`)"
},
{
"name": "index_Feed_account_id",
"unique": false,
"columnNames": [
"account_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_Feed_account_id` ON `${TABLE_NAME}` (`account_id`)"
}
],
"foreignKeys": [
{
"table": "Folder",
"onDelete": "SET NULL",
"onUpdate": "NO ACTION",
"columns": [
"folder_id"
],
"referencedColumns": [
"id"
]
},
{
"table": "Account",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"account_id"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "Item",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT, `description` TEXT, `clean_description` TEXT, `link` TEXT, `image_link` TEXT, `author` TEXT, `pub_date` INTEGER, `content` TEXT, `feed_id` INTEGER NOT NULL, `read_time` REAL NOT NULL, `read` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `remote_id` TEXT, FOREIGN KEY(`feed_id`) REFERENCES `Feed`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "cleanDescription",
"columnName": "clean_description",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "link",
"columnName": "link",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "imageLink",
"columnName": "image_link",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "author",
"columnName": "author",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "pubDate",
"columnName": "pub_date",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "feedId",
"columnName": "feed_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "readTime",
"columnName": "read_time",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "isRead",
"columnName": "read",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isStarred",
"columnName": "starred",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "remoteId",
"columnName": "remote_id",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_Item_feed_id",
"unique": false,
"columnNames": [
"feed_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_Item_feed_id` ON `${TABLE_NAME}` (`feed_id`)"
}
],
"foreignKeys": [
{
"table": "Feed",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"feed_id"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "Folder",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `remoteId` TEXT, `account_id` INTEGER NOT NULL, FOREIGN KEY(`account_id`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "remoteId",
"columnName": "remoteId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "accountId",
"columnName": "account_id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_Folder_account_id",
"unique": false,
"columnNames": [
"account_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_Folder_account_id` ON `${TABLE_NAME}` (`account_id`)"
}
],
"foreignKeys": [
{
"table": "Account",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"account_id"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "Account",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT, `name` TEXT, `displayed_name` TEXT, `type` TEXT, `last_modified` INTEGER NOT NULL, `current_account` INTEGER NOT NULL, `token` TEXT, `write_token` TEXT, `notifications_enabled` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "displayedName",
"columnName": "displayed_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastModified",
"columnName": "last_modified",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isCurrentAccount",
"columnName": "current_account",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "writeToken",
"columnName": "write_token",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isNotificationsEnabled",
"columnName": "notifications_enabled",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "ItemStateChange",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `read_change` INTEGER NOT NULL, `star_change` INTEGER NOT NULL, `account_id` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`account_id`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "readChange",
"columnName": "read_change",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "starChange",
"columnName": "star_change",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "account_id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": [
{
"table": "Account",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"account_id"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "ItemState",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `read` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `remote_id` TEXT NOT NULL, `account_id` INTEGER NOT NULL, FOREIGN KEY(`account_id`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "read",
"columnName": "read",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "starred",
"columnName": "starred",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "remoteId",
"columnName": "remote_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "account_id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_ItemState_remote_id_account_id",
"unique": true,
"columnNames": [
"remote_id",
"account_id"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ItemState_remote_id_account_id` ON `${TABLE_NAME}` (`remote_id`, `account_id`)"
}
],
"foreignKeys": [
{
"table": "Account",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"account_id"
],
"referencedColumns": [
"id"
]
}
]
}
],
"views": [],
"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, '0bac941f8b1b6003c35a6d0cdc1f2e13')"
]
}
}

View File

@ -54,4 +54,16 @@ class MigrationsTest {
assertEquals("guid", remoteId)
}
}
@Test
fun migrate4To5() {
helper.createDatabase(dbName, 4).apply {
execSQL("Insert Into Account(account_type, last_modified, current_account, notifications_enabled) Values(0, 0, 1, 0)")
}
helper.runMigrationsAndValidate(dbName, 5, true, MigrationFrom4To5).apply {
val type = compileStatement("Select type From Account").simpleQueryForString()
assertEquals("LOCAL", type)
}
}
}

View File

@ -28,7 +28,7 @@ class FeedDaoTest {
val context = ApplicationProvider.getApplicationContext<Context>()
database = Room.inMemoryDatabaseBuilder(context, Database::class.java).build()
account = Account(accountType = AccountType.LOCAL).apply {
account = Account(type = AccountType.LOCAL).apply {
id = database.accountDao().insert(this).toInt()
}

View File

@ -28,7 +28,7 @@ class FolderDaoTest {
val context = ApplicationProvider.getApplicationContext<Context>()
database = Room.inMemoryDatabaseBuilder(context, Database::class.java).build()
account = Account(accountType = AccountType.LOCAL).apply {
account = Account(type = AccountType.LOCAL).apply {
id = database.accountDao().insert(this).toInt()
}

View File

@ -17,12 +17,13 @@ import com.readrops.db.entities.Item
import com.readrops.db.entities.ItemState
import com.readrops.db.entities.ItemStateChange
import com.readrops.db.entities.account.Account
import com.readrops.db.entities.account.AccountType
import com.readrops.db.util.Converters
@Database(
entities = [Feed::class, Item::class, Folder::class, Account::class,
ItemStateChange::class, ItemState::class],
version = 4
version = 5
)
@TypeConverters(Converters::class)
abstract class Database : RoomDatabase() {
@ -96,5 +97,29 @@ object MigrationFrom3To4 : Migration(3, 4) {
db.execSQL("CREATE INDEX IF NOT EXISTS `index_Feed_folder_id` ON `Feed` (`folder_id`)")
db.execSQL("CREATE INDEX IF NOT EXISTS `index_Feed_account_id` ON `Feed` (`account_id`)")
}
}
object MigrationFrom4To5 : Migration(4, 5) {
override fun migrate(db: SupportSQLiteDatabase) {
// rename account_name to name
// rename account_type to type
// rename writeToken to write_token
db.execSQL("CREATE TABLE IF NOT EXISTS `_new_Account` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT, `name` TEXT, `displayed_name` TEXT, `type` TEXT, `last_modified` INTEGER NOT NULL, `current_account` INTEGER NOT NULL, `token` TEXT, `write_token` TEXT, `notifications_enabled` INTEGER NOT NULL)")
db.execSQL("INSERT INTO `_new_Account` (`id`, `url`, `name`, `displayed_name`, `type`, `last_modified`, `current_account`, `token`, `write_token`, `notifications_enabled`) SELECT `id`, `url`, `account_name`, `displayed_name`, NULL, `last_modified`, `current_account`, `token`, `writeToken`, `notifications_enabled` FROM `Account`")
// migrate type from INTEGER to TEXT
val cursor = db.query("SELECT `id`, `account_type` FROM `Account`")
while (cursor.moveToNext()) {
val id = cursor.getInt(0)
val ordinal = cursor.getInt(1)
val type = AccountType.entries[ordinal]
db.execSQL("UPDATE `_new_Account` SET `type` = \"${type.name}\" WHERE `id` = $id")
}
db.execSQL("DROP TABLE IF EXISTS `Account`")
db.execSQL("ALTER TABLE `_new_Account` RENAME TO `Account`")
}
}

View File

@ -7,7 +7,12 @@ val dbModule = module {
single(createdAtStart = true) {
Room.databaseBuilder(get(), Database::class.java, "readrops-db")
.addMigrations(MigrationFrom1To2, MigrationFrom2To3, MigrationFrom3To4)
.addMigrations(
MigrationFrom1To2,
MigrationFrom2To3,
MigrationFrom3To4,
MigrationFrom4To5
)
.build()
}
}

View File

@ -46,6 +46,6 @@ interface AccountDao : BaseDao<Account> {
When id Is Not :accountId Then 0 End""")
suspend fun updateCurrentAccount(accountId: Int)
@Query("Update Account set account_name = :name Where id = :accountId")
@Query("Update Account set name = :name Where id = :accountId")
suspend fun renameAccount(accountId: Int, name: String)
}

View File

@ -10,32 +10,29 @@ import java.io.Serializable
data class Account(
@PrimaryKey(autoGenerate = true) var id: Int = 0,
var url: String? = null,
@ColumnInfo(name = "account_name") var accountName: String? = null,
@ColumnInfo(name = "name") var name: String? = null,
@ColumnInfo(name = "displayed_name") var displayedName: String? = null,
@ColumnInfo(name = "account_type") var accountType: AccountType? = null,
@ColumnInfo(name = "type") var type: AccountType? = null,
@ColumnInfo(name = "last_modified") var lastModified: Long = 0,
@ColumnInfo(name = "current_account") var isCurrentAccount: Boolean = false,
var token: String? = null,
var writeToken: String? = null, // TODO : see if there is a better solution to store specific service account fields
@ColumnInfo(name = "write_token") var writeToken: String? = null,
@ColumnInfo(name = "notifications_enabled") var isNotificationsEnabled: Boolean = false,
@Ignore var login: String? = null,
@Ignore var password: String? = null,
) : Serializable {
constructor(accountUrl: String?, accountName: String, accountType: AccountType):
this(url = accountUrl, accountName = accountName, accountType = accountType)
val config: AccountConfig
get() = accountType!!.accountConfig!!
get() = type!!.config
val isLocal
get() = accountType == AccountType.LOCAL
get() = type == AccountType.LOCAL
fun `is`(accountType: AccountType) = this.accountType == accountType
fun `is`(accountType: AccountType) = this.type == accountType
val loginKey
get() = accountType!!.name + "_login_" + id
get() = type!!.name + "_login_" + id
val passwordKey
get() = accountType!!.name + "_password_" + id
get() = type!!.name + "_password_" + id
}

View File

@ -4,13 +4,13 @@ import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import com.readrops.db.R
// TODO comment Feedly
enum class AccountType(@DrawableRes val iconRes: Int,
@StringRes val typeName: Int,
val accountConfig: AccountConfig?) {
enum class AccountType(
@DrawableRes val iconRes: Int,
@StringRes val nameRes: Int,
val config: AccountConfig
) {
LOCAL(R.mipmap.ic_launcher, R.string.local_account, AccountConfig.LOCAL),
NEXTCLOUD_NEWS(R.drawable.ic_nextcloud_news, R.string.nextcloud_news, AccountConfig.NEXTCLOUD_NEWS),
FEEDLY(R.drawable.ic_feedly, R.string.feedly, null),
FRESHRSS(R.drawable.ic_freshrss, R.string.freshrss, AccountConfig.FRESHRSS),
FEVER(R.drawable.ic_fever, R.string.fever, AccountConfig.FEVER)
}

View File

@ -1,7 +1,6 @@
package com.readrops.db.util
import androidx.room.TypeConverter
import com.readrops.db.entities.account.AccountType
import java.time.LocalDateTime
class Converters {
@ -15,15 +14,4 @@ class Converters {
fun fromLocalDateTime(localDateTime: LocalDateTime): Long {
return localDateTime.toInstant(DateUtils.defaultOffset).toEpochMilli()
}
// TODO Use Room built-in enum converter, ordinal is not reliable
@TypeConverter
fun fromAccountTypeCode(ordinal: Int): AccountType {
return AccountType.entries[ordinal]
}
@TypeConverter
fun getAccountTypeCode(accountType: AccountType): Int {
return accountType.ordinal
}
}