Twelve: database: Add Subsonic providers table

Change-Id: I8451e276b5f7ae8335026d6f3e201369e5c52223
diff --git a/app/schemas/org.lineageos.twelve.database.TwelveDatabase/3.json b/app/schemas/org.lineageos.twelve.database.TwelveDatabase/3.json
new file mode 100644
index 0000000..097715f
--- /dev/null
+++ b/app/schemas/org.lineageos.twelve.database.TwelveDatabase/3.json
@@ -0,0 +1,307 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 3,
+    "identityHash": "f770cb80ab0712cb4fc77db84d40e9a5",
+    "entities": [
+      {
+        "tableName": "Playlist",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlist_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `last_modified` INTEGER NOT NULL, `track_count` INTEGER NOT NULL DEFAULT 0)",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "playlist_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "lastModified",
+            "columnName": "last_modified",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "trackCount",
+            "columnName": "track_count",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "0"
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": true,
+          "columnNames": [
+            "playlist_id"
+          ]
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "Item",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`item_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `audio_uri` TEXT NOT NULL, `count` INTEGER NOT NULL DEFAULT 0)",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "item_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "audioUri",
+            "columnName": "audio_uri",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "count",
+            "columnName": "count",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "0"
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": true,
+          "columnNames": [
+            "item_id"
+          ]
+        },
+        "indices": [
+          {
+            "name": "index_Item_audio_uri",
+            "unique": true,
+            "columnNames": [
+              "audio_uri"
+            ],
+            "orders": [],
+            "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Item_audio_uri` ON `${TABLE_NAME}` (`audio_uri`)"
+          }
+        ],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "PlaylistItemCrossRef",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlist_id` INTEGER NOT NULL, `item_id` INTEGER NOT NULL, `last_modified` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `item_id`), FOREIGN KEY(`playlist_id`) REFERENCES `Playlist`(`playlist_id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`item_id`) REFERENCES `Item`(`item_id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "playlistId",
+            "columnName": "playlist_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "itemId",
+            "columnName": "item_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "lastModified",
+            "columnName": "last_modified",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": false,
+          "columnNames": [
+            "playlist_id",
+            "item_id"
+          ]
+        },
+        "indices": [
+          {
+            "name": "index_PlaylistItemCrossRef_item_id",
+            "unique": false,
+            "columnNames": [
+              "item_id"
+            ],
+            "orders": [],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_PlaylistItemCrossRef_item_id` ON `${TABLE_NAME}` (`item_id`)"
+          }
+        ],
+        "foreignKeys": [
+          {
+            "table": "Playlist",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "playlist_id"
+            ],
+            "referencedColumns": [
+              "playlist_id"
+            ]
+          },
+          {
+            "table": "Item",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "item_id"
+            ],
+            "referencedColumns": [
+              "item_id"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "ResumptionItem",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlist_index` INTEGER NOT NULL, `resumption_playlist_id` INTEGER NOT NULL, `media_id` TEXT NOT NULL, PRIMARY KEY(`playlist_index`), FOREIGN KEY(`resumption_playlist_id`) REFERENCES `ResumptionPlaylist`(`resumption_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "playlistIndex",
+            "columnName": "playlist_index",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "resumptionPlaylistId",
+            "columnName": "resumption_playlist_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "mediaId",
+            "columnName": "media_id",
+            "affinity": "TEXT",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": false,
+          "columnNames": [
+            "playlist_index"
+          ]
+        },
+        "indices": [
+          {
+            "name": "index_ResumptionItem_playlist_index",
+            "unique": true,
+            "columnNames": [
+              "playlist_index"
+            ],
+            "orders": [],
+            "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ResumptionItem_playlist_index` ON `${TABLE_NAME}` (`playlist_index`)"
+          },
+          {
+            "name": "index_ResumptionItem_resumption_playlist_id",
+            "unique": false,
+            "columnNames": [
+              "resumption_playlist_id"
+            ],
+            "orders": [],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_ResumptionItem_resumption_playlist_id` ON `${TABLE_NAME}` (`resumption_playlist_id`)"
+          }
+        ],
+        "foreignKeys": [
+          {
+            "table": "ResumptionPlaylist",
+            "onDelete": "CASCADE",
+            "onUpdate": "NO ACTION",
+            "columns": [
+              "resumption_playlist_id"
+            ],
+            "referencedColumns": [
+              "resumption_id"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "ResumptionPlaylist",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`resumption_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `start_index` INTEGER NOT NULL, `start_position_ms` INTEGER NOT NULL)",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "resumption_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "startIndex",
+            "columnName": "start_index",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "startPositionMs",
+            "columnName": "start_position_ms",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": true,
+          "columnNames": [
+            "resumption_id"
+          ]
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "SubsonicProvider",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`subsonic_provider_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, `use_legacy_authentication` INTEGER NOT NULL)",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "subsonic_provider_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "url",
+            "columnName": "url",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "username",
+            "columnName": "username",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "password",
+            "columnName": "password",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "useLegacyAuthentication",
+            "columnName": "use_legacy_authentication",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": true,
+          "columnNames": [
+            "subsonic_provider_id"
+          ]
+        },
+        "indices": [],
+        "foreignKeys": []
+      }
+    ],
+    "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, 'f770cb80ab0712cb4fc77db84d40e9a5')"
+    ]
+  }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/lineageos/twelve/database/TwelveDatabase.kt b/app/src/main/java/org/lineageos/twelve/database/TwelveDatabase.kt
index e125ed5..f7a5202 100644
--- a/app/src/main/java/org/lineageos/twelve/database/TwelveDatabase.kt
+++ b/app/src/main/java/org/lineageos/twelve/database/TwelveDatabase.kt
@@ -17,11 +17,13 @@
 import org.lineageos.twelve.database.dao.PlaylistItemCrossRefDao
 import org.lineageos.twelve.database.dao.PlaylistWithItemsDao
 import org.lineageos.twelve.database.dao.ResumptionPlaylistDao
+import org.lineageos.twelve.database.dao.SubsonicProviderDao
 import org.lineageos.twelve.database.entities.Item
 import org.lineageos.twelve.database.entities.Playlist
 import org.lineageos.twelve.database.entities.PlaylistItemCrossRef
 import org.lineageos.twelve.database.entities.ResumptionItem
 import org.lineageos.twelve.database.entities.ResumptionPlaylist
+import org.lineageos.twelve.database.entities.SubsonicProvider
 
 @Database(
     entities = [
@@ -33,10 +35,14 @@
         /* Resumption */
         ResumptionItem::class,
         ResumptionPlaylist::class,
+
+        /* Providers */
+        SubsonicProvider::class,
     ],
-    version = 2,
+    version = 3,
     autoMigrations = [
         AutoMigration(from = 1, to = 2),
+        AutoMigration(from = 2, to = 3),
     ],
 )
 @TypeConverters(UriConverter::class)
@@ -46,6 +52,7 @@
     abstract fun getPlaylistItemCrossRefDao(): PlaylistItemCrossRefDao
     abstract fun getPlaylistWithItemsDao(): PlaylistWithItemsDao
     abstract fun getResumptionPlaylistDao(): ResumptionPlaylistDao
+    abstract fun getSubsonicProviderDao(): SubsonicProviderDao
 
     companion object {
         @Volatile
diff --git a/app/src/main/java/org/lineageos/twelve/database/dao/SubsonicProviderDao.kt b/app/src/main/java/org/lineageos/twelve/database/dao/SubsonicProviderDao.kt
new file mode 100644
index 0000000..273f56e
--- /dev/null
+++ b/app/src/main/java/org/lineageos/twelve/database/dao/SubsonicProviderDao.kt
@@ -0,0 +1,72 @@
+/*
+ * SPDX-FileCopyrightText: 2024 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.lineageos.twelve.database.dao
+
+import androidx.room.Dao
+import androidx.room.Query
+import kotlinx.coroutines.flow.Flow
+import org.lineageos.twelve.database.entities.SubsonicProvider
+
+@Dao
+interface SubsonicProviderDao {
+    /**
+     * Add a new subsonic provider to the database.
+     */
+    @Query(
+        """
+            INSERT INTO SubsonicProvider (name, url, username, password, use_legacy_authentication)
+            VALUES (:name, :url, :username, :password, :useLegacyAuthentication)
+        """
+    )
+    suspend fun create(
+        name: String,
+        url: String,
+        username: String,
+        password: String,
+        useLegacyAuthentication: Boolean,
+    ): Long
+
+    /**
+     * Update a subsonic provider.
+     */
+    @Query(
+        """
+            UPDATE SubsonicProvider
+            SET name = :name,
+                url = :url,
+                username = :username,
+                password = :password,
+                use_legacy_authentication = :useLegacyAuthentication
+            WHERE subsonic_provider_id = :subsonicProviderId
+        """
+    )
+    suspend fun update(
+        subsonicProviderId: Long,
+        name: String,
+        url: String,
+        username: String,
+        password: String,
+        useLegacyAuthentication: Boolean,
+    )
+
+    /**
+     * Delete a subsonic provider from the database.
+     */
+    @Query("DELETE FROM SubsonicProvider WHERE subsonic_provider_id = :subsonicProviderId")
+    suspend fun delete(subsonicProviderId: Long)
+
+    /**
+     * Fetch all subsonic providers from the database.
+     */
+    @Query("SELECT * FROM SubsonicProvider")
+    fun getAll(): Flow<List<SubsonicProvider>>
+
+    /**
+     * Fetch a subsonic provider by its ID from the database.
+     */
+    @Query("SELECT * FROM SubsonicProvider WHERE subsonic_provider_id = :subsonicProviderId")
+    fun getById(subsonicProviderId: Long): Flow<SubsonicProvider?>
+}
diff --git a/app/src/main/java/org/lineageos/twelve/database/entities/SubsonicProvider.kt b/app/src/main/java/org/lineageos/twelve/database/entities/SubsonicProvider.kt
new file mode 100644
index 0000000..c6ae45d
--- /dev/null
+++ b/app/src/main/java/org/lineageos/twelve/database/entities/SubsonicProvider.kt
@@ -0,0 +1,23 @@
+/*
+ * SPDX-FileCopyrightText: 2024 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.lineageos.twelve.database.entities
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+/**
+ * Subsonic provider entity.
+ */
+@Entity
+data class SubsonicProvider(
+    @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "subsonic_provider_id") val id: Long,
+    @ColumnInfo(name = "name") val name: String,
+    @ColumnInfo(name = "url") val url: String,
+    @ColumnInfo(name = "username") val username: String,
+    @ColumnInfo(name = "password") val password: String,
+    @ColumnInfo(name = "use_legacy_authentication") val useLegacyAuthentication: Boolean,
+)