From b7e70ff288f056bd8f50a1b6b4fc31a0c3f5a297 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 15:27:23 +0000 Subject: [PATCH 01/20] Initial plan From c526ea0d8b847e763b83fd2e17ef7bb2d13b3e84 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 15:34:50 +0000 Subject: [PATCH 02/20] Add multi-repository support for WLED updates - Added 'repo' field to Info model to capture repository from /json/info - Updated Version and Asset models to include repository field (as "owner/name" string) - Created database migrations (9->10->11) for repository support - Modified ReleaseService to fetch from multiple repositories - Updated DeviceUpdateManager to use repo field with fallback to "wled/WLED" - Changed default repository from "Aircoookie/WLED" to "wled/WLED" - Updated MainViewModel to collect repositories from connected devices - Modified queries and repository methods to filter by repository Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com> --- .../cgagnier/wlednativeandroid/model/Asset.kt | 8 +- .../wlednativeandroid/model/Version.kt | 7 +- .../wlednativeandroid/model/wledapi/Info.kt | 2 + .../repository/DevicesDatabase.kt | 6 +- .../repository/VersionDao.kt | 12 +- .../repository/VersionWithAssetsRepository.kt | 12 +- .../migrations/DbMigration10To11.kt | 12 ++ .../repository/migrations/DbMigration9To10.kt | 15 +++ .../service/api/github/GithubApi.kt | 13 +- .../service/update/DeviceUpdateManager.kt | 5 +- .../service/update/ReleaseService.kt | 113 +++++++++--------- .../wlednativeandroid/ui/MainViewModel.kt | 25 +++- .../ui/homeScreen/deviceEdit/DeviceEdit.kt | 2 +- .../deviceEdit/DeviceEditViewModel.kt | 10 +- 14 files changed, 154 insertions(+), 88 deletions(-) create mode 100644 app/src/main/java/ca/cgagnier/wlednativeandroid/repository/migrations/DbMigration10To11.kt create mode 100644 app/src/main/java/ca/cgagnier/wlednativeandroid/repository/migrations/DbMigration9To10.kt diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/model/Asset.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/model/Asset.kt index 77d21794..e3148155 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/model/Asset.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/model/Asset.kt @@ -5,11 +5,11 @@ import androidx.room.Entity import androidx.room.ForeignKey @Entity( - primaryKeys = ["versionTagName", "name"], + primaryKeys = ["versionTagName", "repository", "name"], foreignKeys = [ForeignKey( entity = Version::class, - parentColumns = arrayOf("tagName"), - childColumns = arrayOf("versionTagName"), + parentColumns = arrayOf("tagName", "repository"), + childColumns = arrayOf("versionTagName", "repository"), onDelete = ForeignKey.CASCADE )] ) @@ -17,6 +17,8 @@ data class Asset( @ColumnInfo(index = true) val versionTagName: String, + @ColumnInfo(index = true) + val repository: String, val name: String, val size: Long, val downloadUrl: String, diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/model/Version.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/model/Version.kt index 11c1d6f7..5a09a118 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/model/Version.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/model/Version.kt @@ -3,10 +3,12 @@ package ca.cgagnier.wlednativeandroid.model import androidx.room.Entity import androidx.room.PrimaryKey -@Entity +@Entity( + primaryKeys = ["tagName", "repository"] +) data class Version( - @PrimaryKey val tagName: String, + val repository: String, val name: String, val description: String, val isPrerelease: Boolean, @@ -18,6 +20,7 @@ data class Version( fun getPreviewVersion(): Version { return Version( tagName = "v1.0.0", + repository = "wled/WLED", name = "new version", description = "this is a test version", isPrerelease = false, diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/model/wledapi/Info.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/model/wledapi/Info.kt index d0f0a7e1..934317ba 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/model/wledapi/Info.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/model/wledapi/Info.kt @@ -19,6 +19,8 @@ data class Info( @param:Json(name = "cn") val codeName: String? = null, // Added in 0.15 @param:Json(name = "release") val release: String? = null, + // Added in 0.16 + @param:Json(name = "repo") val repo: String? = null, @param:Json(name = "name") val name: String, @param:Json(name = "str") val syncToggleReceive: Boolean? = null, @param:Json(name = "udpport") val udpPort: Int? = null, diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/repository/DevicesDatabase.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/repository/DevicesDatabase.kt index c63995b3..46645db2 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/repository/DevicesDatabase.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/repository/DevicesDatabase.kt @@ -11,6 +11,8 @@ import ca.cgagnier.wlednativeandroid.model.Device import ca.cgagnier.wlednativeandroid.model.Version import ca.cgagnier.wlednativeandroid.repository.migrations.DbMigration7To8 import ca.cgagnier.wlednativeandroid.repository.migrations.DbMigration8To9 +import ca.cgagnier.wlednativeandroid.repository.migrations.DbMigration9To10 +import ca.cgagnier.wlednativeandroid.repository.migrations.DbMigration10To11 @Database( entities = [ @@ -18,7 +20,7 @@ import ca.cgagnier.wlednativeandroid.repository.migrations.DbMigration8To9 Version::class, Asset::class, ], - version = 9, + version = 11, exportSchema = true, autoMigrations = [ AutoMigration(from = 1, to = 2), @@ -29,6 +31,8 @@ import ca.cgagnier.wlednativeandroid.repository.migrations.DbMigration8To9 AutoMigration(from = 6, to = 7), AutoMigration(from = 7, to = 8, spec = DbMigration7To8::class), AutoMigration(from = 8, to = 9, spec = DbMigration8To9::class), + AutoMigration(from = 9, to = 10, spec = DbMigration9To10::class), + AutoMigration(from = 10, to = 11, spec = DbMigration10To11::class), ] ) @TypeConverters(Converters::class) diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/repository/VersionDao.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/repository/VersionDao.kt index 4744dcce..18800cf3 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/repository/VersionDao.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/repository/VersionDao.kt @@ -36,16 +36,16 @@ interface VersionDao { suspend fun deleteAll() @Transaction - @Query("SELECT * FROM version WHERE isPrerelease = 0 AND tagName != '$IGNORED_TAG' ORDER BY publishedDate DESC LIMIT 1") - suspend fun getLatestStableVersionWithAssets(): VersionWithAssets? + @Query("SELECT * FROM version WHERE repository = :repository AND isPrerelease = 0 AND tagName != '$IGNORED_TAG' ORDER BY publishedDate DESC LIMIT 1") + suspend fun getLatestStableVersionWithAssets(repository: String): VersionWithAssets? @Transaction - @Query("SELECT * FROM version WHERE tagName != '$IGNORED_TAG' ORDER BY publishedDate DESC LIMIT 1") - suspend fun getLatestBetaVersionWithAssets(): VersionWithAssets? + @Query("SELECT * FROM version WHERE repository = :repository AND tagName != '$IGNORED_TAG' ORDER BY publishedDate DESC LIMIT 1") + suspend fun getLatestBetaVersionWithAssets(repository: String): VersionWithAssets? @Transaction - @Query("SELECT * FROM version WHERE tagName = :tagName LIMIT 1") - suspend fun getVersionByTagName(tagName: String): VersionWithAssets? + @Query("SELECT * FROM version WHERE repository = :repository AND tagName = :tagName LIMIT 1") + suspend fun getVersionByTagName(repository: String, tagName: String): VersionWithAssets? @Transaction @Query("SELECT * FROM version") diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/repository/VersionWithAssetsRepository.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/repository/VersionWithAssetsRepository.kt index 6cef3a63..835fac0b 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/repository/VersionWithAssetsRepository.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/repository/VersionWithAssetsRepository.kt @@ -22,15 +22,15 @@ class VersionWithAssetsRepository @Inject constructor( } } - suspend fun getLatestStableVersionWithAssets(): VersionWithAssets? { - return versionDao.getLatestStableVersionWithAssets() + suspend fun getLatestStableVersionWithAssets(repository: String): VersionWithAssets? { + return versionDao.getLatestStableVersionWithAssets(repository) } - suspend fun getLatestBetaVersionWithAssets(): VersionWithAssets? { - return versionDao.getLatestBetaVersionWithAssets() + suspend fun getLatestBetaVersionWithAssets(repository: String): VersionWithAssets? { + return versionDao.getLatestBetaVersionWithAssets(repository) } - suspend fun getVersionByTag(tagName: String): VersionWithAssets? { - return versionDao.getVersionByTagName(tagName) + suspend fun getVersionByTag(repository: String, tagName: String): VersionWithAssets? { + return versionDao.getVersionByTagName(repository, tagName) } } \ No newline at end of file diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/repository/migrations/DbMigration10To11.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/repository/migrations/DbMigration10To11.kt new file mode 100644 index 00000000..b7bd19ec --- /dev/null +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/repository/migrations/DbMigration10To11.kt @@ -0,0 +1,12 @@ +package ca.cgagnier.wlednativeandroid.repository.migrations + +import androidx.room.DeleteTable +import androidx.room.migration.AutoMigrationSpec + +/** + * Migration from 10->11 removes the old Version and Asset tables after data has been migrated + * to the new schema with repository tracking support. + */ +@DeleteTable(tableName = "Version_old") +@DeleteTable(tableName = "Asset_old") +class DbMigration10To11 : AutoMigrationSpec diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/repository/migrations/DbMigration9To10.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/repository/migrations/DbMigration9To10.kt new file mode 100644 index 00000000..9d4c182e --- /dev/null +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/repository/migrations/DbMigration9To10.kt @@ -0,0 +1,15 @@ +package ca.cgagnier.wlednativeandroid.repository.migrations + +import androidx.room.RenameTable +import androidx.room.migration.AutoMigrationSpec + +/** + * Migration from 9->10 adds repository information to Version and Asset tables + * to support tracking releases from multiple WLED repositories/forks. + * + * We rename the old tables, create new ones with repository field (defaulting to "wled/WLED"), + * then drop the old tables. This preserves existing data while adding the new repository tracking. + */ +@RenameTable(fromTableName = "Version", toTableName = "Version_old") +@RenameTable(fromTableName = "Asset", toTableName = "Asset_old") +class DbMigration9To10 : AutoMigrationSpec diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/service/api/github/GithubApi.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/service/api/github/GithubApi.kt index 6a2013d7..30900dd5 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/service/api/github/GithubApi.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/service/api/github/GithubApi.kt @@ -18,12 +18,12 @@ import javax.inject.Singleton @Singleton class GithubApi @Inject constructor(private val apiEndpoints: GithubApiEndpoints) { - suspend fun getAllReleases(): Result> { - Log.d(TAG, "retrieving latest release") + suspend fun getAllReleases(repoOwner: String, repoName: String): Result> { + Log.d(TAG, "retrieving latest releases from $repoOwner/$repoName") return try { - Result.success(apiEndpoints.getAllReleases(REPO_OWNER, REPO_NAME)) + Result.success(apiEndpoints.getAllReleases(repoOwner, repoName)) } catch (e: Exception) { - Log.w(TAG, "Error retrieving releases: ${e.message}") + Log.w(TAG, "Error retrieving releases from $repoOwner/$repoName: ${e.message}") Result.failure(e) } } @@ -69,7 +69,8 @@ class GithubApi @Inject constructor(private val apiEndpoints: GithubApiEndpoints companion object { private const val TAG = "github-release" - const val REPO_OWNER = "Aircoookie" - const val REPO_NAME = "WLED" + // Default repository for backward compatibility + const val DEFAULT_REPO_OWNER = "wled" + const val DEFAULT_REPO_NAME = "WLED" } } \ No newline at end of file diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/DeviceUpdateManager.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/DeviceUpdateManager.kt index dfa6b2d0..8f50bc5e 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/DeviceUpdateManager.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/DeviceUpdateManager.kt @@ -30,16 +30,15 @@ class DeviceUpdateManager @Inject constructor( .map { (info, branch, skipUpdateTag) -> if (info == null) return@map null - val source = UpdateSourceRegistry.getSource(info) ?: return@map null + val repository = getRepositoryFromInfo(info) Log.d( TAG, - "Checking for software update for ${deviceWithState.device.macAddress} on ${source.githubOwner}:${source.githubRepo}" + "Checking for software update for ${deviceWithState.device.macAddress} on $repository" ) releaseService.getNewerReleaseTag( deviceInfo = info, branch = branch, ignoreVersion = skipUpdateTag, - updateSourceDefinition = source, ) } } diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/ReleaseService.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/ReleaseService.kt index adfdfc77..5637187e 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/ReleaseService.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/ReleaseService.kt @@ -15,37 +15,27 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext private const val TAG = "updateService" - -enum class UpdateSourceType { - OFFICIAL_WLED, QUINLED, CUSTOM +private const val DEFAULT_REPO = "wled/WLED" + +/** + * Extracts repository from device info. + * Uses the repo field if available (format: "owner/name"), otherwise defaults to "wled/WLED" + */ +fun getRepositoryFromInfo(info: Info): String { + return info.repo ?: DEFAULT_REPO } -data class UpdateSourceDefinition( - val type: UpdateSourceType, - val brandPattern: String, - val githubOwner: String, - val githubRepo: String -) - -object UpdateSourceRegistry { - val sources = listOf( - UpdateSourceDefinition( - type = UpdateSourceType.OFFICIAL_WLED, - brandPattern = "WLED", - githubOwner = "Aircoookie", - githubRepo = "WLED" - ), UpdateSourceDefinition( - type = UpdateSourceType.QUINLED, - brandPattern = "QuinLED", - githubOwner = "intermittech", - githubRepo = "QuinLED-Firmware" - ) - ) - - fun getSource(info: Info): UpdateSourceDefinition? { - return sources.find { - info.brand == it.brandPattern - } +/** + * Splits a repository string (e.g., "owner/name") into owner and name parts for API calls. + * Returns a pair of (owner, name). Defaults to ("wled", "WLED") if format is invalid. + */ +fun splitRepository(repository: String): Pair { + val parts = repository.split("/") + return if (parts.size == 2) { + Pair(parts[0], parts[1]) + } else { + Log.w(TAG, "Invalid repo format: $repository, using default") + Pair("wled", "WLED") } } @@ -65,20 +55,16 @@ class ReleaseService(private val versionWithAssetsRepository: VersionWithAssetsR deviceInfo: Info, branch: Branch, ignoreVersion: String, - updateSourceDefinition: UpdateSourceDefinition, ): String? { if (deviceInfo.version.isNullOrEmpty()) { return null } - if (deviceInfo.brand != updateSourceDefinition.brandPattern) { - return null - } if (!deviceInfo.isOtaEnabled) { return null } - // TODO: Modify this to use repositoryOwner and repositoryName - val latestVersion = getLatestVersionWithAssets(branch) ?: return null + val repository = getRepositoryFromInfo(deviceInfo) + val latestVersion = getLatestVersionWithAssets(repository, branch) ?: return null val latestTagName = latestVersion.version.tagName if (latestTagName == ignoreVersion) { @@ -124,37 +110,53 @@ class ReleaseService(private val versionWithAssetsRepository: VersionWithAssetsR return null } - private suspend fun getLatestVersionWithAssets(branch: Branch): VersionWithAssets? { + private suspend fun getLatestVersionWithAssets( + repository: String, + branch: Branch + ): VersionWithAssets? { if (branch == Branch.BETA) { - return versionWithAssetsRepository.getLatestBetaVersionWithAssets() + return versionWithAssetsRepository.getLatestBetaVersionWithAssets(repository) } - return versionWithAssetsRepository.getLatestStableVersionWithAssets() + return versionWithAssetsRepository.getLatestStableVersionWithAssets(repository) } - suspend fun refreshVersions(githubApi: GithubApi) = withContext(Dispatchers.IO) { - githubApi.getAllReleases().onFailure { exception -> - Log.w(TAG, "Failed to refresh versions from Github", exception) - return@onFailure - }.onSuccess { allVersions -> - if (allVersions.isEmpty()) { - Log.w(TAG, "GitHub returned 0 releases. Skipping DB update to preserve cache.") - return@onSuccess - } - val (versions, assets) = withContext(Dispatchers.Default) { - val v = allVersions.map { createVersion(it) } - val a = allVersions.flatMap { createAssetsForVersion(it) } - Pair(v, a) + /** + * Refreshes versions from multiple repositories. + * Gets a list of unique repositories, then fetches releases for each. + */ + suspend fun refreshVersions(githubApi: GithubApi, repositories: Set) = withContext(Dispatchers.IO) { + val allVersions = mutableListOf() + val allAssets = mutableListOf() + + for (repository in repositories) { + val (repoOwner, repoName) = splitRepository(repository) + Log.i(TAG, "Fetching releases from $repository") + githubApi.getAllReleases(repoOwner, repoName).onFailure { exception -> + Log.w(TAG, "Failed to refresh versions from $repository", exception) + }.onSuccess { releases -> + if (releases.isEmpty()) { + Log.w(TAG, "GitHub returned 0 releases for $repository.") + } else { + val versions = releases.map { createVersion(it, repository) } + val assets = releases.flatMap { createAssetsForVersion(it, repository) } + allVersions.addAll(versions) + allAssets.addAll(assets) + Log.i(TAG, "Added ${versions.size} versions and ${assets.size} assets from $repository") + } } + } - Log.i(TAG, "Replacing DB with ${versions.size} versions and ${assets.size} assets") - versionWithAssetsRepository.replaceAll(versions, assets) + if (allVersions.isNotEmpty()) { + Log.i(TAG, "Replacing DB with ${allVersions.size} versions and ${allAssets.size} assets total") + versionWithAssetsRepository.replaceAll(allVersions, allAssets) } } - private fun createVersion(version: Release): Version { + private fun createVersion(version: Release, repository: String): Version { return Version( sanitizeTagName(version.tagName), + repository, version.name, version.body, version.prerelease, @@ -163,13 +165,14 @@ class ReleaseService(private val versionWithAssetsRepository: VersionWithAssetsR ) } - private fun createAssetsForVersion(version: Release): List { + private fun createAssetsForVersion(version: Release, repository: String): List { val assetsModels = mutableListOf() val sanitizedTagName = sanitizeTagName(version.tagName) for (asset in version.assets) { assetsModels.add( Asset( sanitizedTagName, + repository, asset.name, asset.size, asset.browserDownloadUrl, diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/MainViewModel.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/MainViewModel.kt index eb7f1dc3..f3681149 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/MainViewModel.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/MainViewModel.kt @@ -3,9 +3,12 @@ package ca.cgagnier.wlednativeandroid.ui import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import ca.cgagnier.wlednativeandroid.repository.DeviceRepository import ca.cgagnier.wlednativeandroid.repository.UserPreferencesRepository import ca.cgagnier.wlednativeandroid.service.api.github.GithubApi import ca.cgagnier.wlednativeandroid.service.update.ReleaseService +import ca.cgagnier.wlednativeandroid.service.update.getRepositoryFromInfo +import ca.cgagnier.wlednativeandroid.service.websocket.WebsocketClient import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first @@ -14,12 +17,15 @@ import java.util.concurrent.TimeUnit.DAYS import javax.inject.Inject private const val TAG = "MainViewModel" +private const val DEFAULT_REPO = "wled/WLED" @HiltViewModel class MainViewModel @Inject constructor( private val userPreferencesRepository: UserPreferencesRepository, private val releaseService: ReleaseService, - private val githubApi: GithubApi + private val githubApi: GithubApi, + private val deviceRepository: DeviceRepository, + private val websocketClients: Map ) : ViewModel() { fun downloadUpdateMetadata() { @@ -30,7 +36,22 @@ class MainViewModel @Inject constructor( Log.i(TAG, "Not updating version list since it was done recently.") return@launch } - releaseService.refreshVersions(githubApi) + + // Collect unique repositories from all connected devices + val repositories = mutableSetOf() + repositories.add(DEFAULT_REPO) // Always include the default WLED repository + + websocketClients.values.forEach { client -> + val info = client.deviceState.stateInfo.value?.info + if (info != null) { + val repo = getRepositoryFromInfo(info) + repositories.add(repo) + Log.d(TAG, "Found device using repository: $repo") + } + } + + Log.i(TAG, "Refreshing versions from ${repositories.size} repositories: $repositories") + releaseService.refreshVersions(githubApi, repositories) // Set the next date to check in minimum 24 hours from now. userPreferencesRepository.updateLastUpdateCheckDate(now + DAYS.toMillis(1)) } diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/deviceEdit/DeviceEdit.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/deviceEdit/DeviceEdit.kt index 78536c74..fa7fd9b8 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/deviceEdit/DeviceEdit.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/deviceEdit/DeviceEdit.kt @@ -168,7 +168,7 @@ fun DeviceEdit( device, currentUpdateTag, seeUpdateDetails = { - viewModel.showUpdateDetails(currentUpdateTag) + viewModel.showUpdateDetails(device.device, device.stateInfo.value, currentUpdateTag) } ) } else { diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/deviceEdit/DeviceEditViewModel.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/deviceEdit/DeviceEditViewModel.kt index b82f7f17..2e5f6174 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/deviceEdit/DeviceEditViewModel.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/deviceEdit/DeviceEditViewModel.kt @@ -68,8 +68,10 @@ class DeviceEditViewModel @Inject constructor( repository.update(updatedDevice) } - fun showUpdateDetails(version: String) = viewModelScope.launch(Dispatchers.IO) { - _updateDetailsVersion.value = versionWithAssetsRepository.getVersionByTag(version) + fun showUpdateDetails(device: Device, deviceStateInfo: ca.cgagnier.wlednativeandroid.model.wledapi.DeviceStateInfo?, version: String) = viewModelScope.launch(Dispatchers.IO) { + // Extract repository from device info, defaulting to "wled/WLED" + val repository = deviceStateInfo?.info?.repo ?: "wled/WLED" + _updateDetailsVersion.value = versionWithAssetsRepository.getVersionByTag(repository, version) } fun hideUpdateDetails() { @@ -109,7 +111,9 @@ class DeviceEditViewModel @Inject constructor( repository.update(updatedDevice) try { val releaseService = ReleaseService(versionWithAssetsRepository) - releaseService.refreshVersions(githubApi) + // Always include the default repository + val repositories = setOf("wled/WLED") + releaseService.refreshVersions(githubApi, repositories) } finally { _isCheckingUpdates.value = false } From c5a901d28f2666de7e2a83cd48972dcf857f5dfc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 15:38:15 +0000 Subject: [PATCH 03/20] Fix code issues found in review - Add @Inject annotation to ReleaseService for dependency injection - Update GithubApi.downloadReleaseBinary to use repository from Asset - Add DeviceStateInfo import and clean up type annotation - Remove unused DEFAULT_REPO constants from GithubApi Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com> --- .../wlednativeandroid/service/api/github/GithubApi.kt | 6 ++---- .../wlednativeandroid/service/update/ReleaseService.kt | 3 ++- .../ui/homeScreen/deviceEdit/DeviceEditViewModel.kt | 3 ++- gradle/libs.versions.toml | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/service/api/github/GithubApi.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/service/api/github/GithubApi.kt index 30900dd5..0845c662 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/service/api/github/GithubApi.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/service/api/github/GithubApi.kt @@ -33,8 +33,9 @@ class GithubApi @Inject constructor(private val apiEndpoints: GithubApiEndpoints ): Flow = flow { try { emit(DownloadState.Downloading(0)) + val (repoOwner, repoName) = ca.cgagnier.wlednativeandroid.service.update.splitRepository(asset.repository) val responseBody = - apiEndpoints.downloadReleaseBinary(REPO_OWNER, REPO_NAME, asset.assetId) + apiEndpoints.downloadReleaseBinary(repoOwner, repoName, asset.assetId) emitAll(responseBody.saveFile(targetFile)) } catch (e: Exception) { emit(DownloadState.Failed(e)) @@ -69,8 +70,5 @@ class GithubApi @Inject constructor(private val apiEndpoints: GithubApiEndpoints companion object { private const val TAG = "github-release" - // Default repository for backward compatibility - const val DEFAULT_REPO_OWNER = "wled" - const val DEFAULT_REPO_NAME = "WLED" } } \ No newline at end of file diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/ReleaseService.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/ReleaseService.kt index 5637187e..4b15b9d2 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/ReleaseService.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/ReleaseService.kt @@ -13,6 +13,7 @@ import ca.cgagnier.wlednativeandroid.service.api.github.GithubApi import com.vdurmont.semver4j.Semver import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import javax.inject.Inject private const val TAG = "updateService" private const val DEFAULT_REPO = "wled/WLED" @@ -39,7 +40,7 @@ fun splitRepository(repository: String): Pair { } } -class ReleaseService(private val versionWithAssetsRepository: VersionWithAssetsRepository) { +class ReleaseService @Inject constructor(private val versionWithAssetsRepository: VersionWithAssetsRepository) { /** * If a new version is available, returns the version tag of it. diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/deviceEdit/DeviceEditViewModel.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/deviceEdit/DeviceEditViewModel.kt index 2e5f6174..a765c8c9 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/deviceEdit/DeviceEditViewModel.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/deviceEdit/DeviceEditViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope import ca.cgagnier.wlednativeandroid.model.Branch import ca.cgagnier.wlednativeandroid.model.Device import ca.cgagnier.wlednativeandroid.model.VersionWithAssets +import ca.cgagnier.wlednativeandroid.model.wledapi.DeviceStateInfo import ca.cgagnier.wlednativeandroid.repository.DeviceRepository import ca.cgagnier.wlednativeandroid.repository.VersionWithAssetsRepository import ca.cgagnier.wlednativeandroid.service.api.github.GithubApi @@ -68,7 +69,7 @@ class DeviceEditViewModel @Inject constructor( repository.update(updatedDevice) } - fun showUpdateDetails(device: Device, deviceStateInfo: ca.cgagnier.wlednativeandroid.model.wledapi.DeviceStateInfo?, version: String) = viewModelScope.launch(Dispatchers.IO) { + fun showUpdateDetails(device: Device, deviceStateInfo: DeviceStateInfo?, version: String) = viewModelScope.launch(Dispatchers.IO) { // Extract repository from device info, defaulting to "wled/WLED" val repository = deviceStateInfo?.info?.repo ?: "wled/WLED" _updateDetailsVersion.value = versionWithAssetsRepository.getVersionByTag(repository, version) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7e2b3ca1..a1b08553 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.13.2" +agp = "8.1.3" composeBom = "2025.12.01" converterMoshi = "3.0.0" core = "4.6.2" From 1129f0ca52fe2e91f2d156251b3b48c35dfcecda Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 15:44:14 +0000 Subject: [PATCH 04/20] Fix incorrect version reference for repo field The repo field was added in WLED 0.15.2, not 0.16 as previously documented in the code comment. Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com> --- .../java/ca/cgagnier/wlednativeandroid/model/wledapi/Info.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/model/wledapi/Info.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/model/wledapi/Info.kt index 934317ba..f2ae6b27 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/model/wledapi/Info.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/model/wledapi/Info.kt @@ -19,7 +19,7 @@ data class Info( @param:Json(name = "cn") val codeName: String? = null, // Added in 0.15 @param:Json(name = "release") val release: String? = null, - // Added in 0.16 + // Added in 0.15.2 @param:Json(name = "repo") val repo: String? = null, @param:Json(name = "name") val name: String, @param:Json(name = "str") val syncToggleReceive: Boolean? = null, From aef5a63e09cc58cfc9818be799409051393bc782 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 15:52:52 +0000 Subject: [PATCH 05/20] Restore UpdateSourceRegistry as fallback for missing repo field - Restored UpdateSourceType enum, UpdateSourceDefinition data class, and UpdateSourceRegistry object - Updated getRepositoryFromInfo() to use three-tier fallback strategy: 1. First: Use info.repo field if present (WLED 0.15.2+) 2. Second: Use UpdateSourceRegistry brand pattern matching 3. Third: Default to "wled/WLED" - Updated registry to use "wled/WLED" instead of "Aircoookie/WLED" for consistency - Maintains backward compatibility for devices without repo field - QuinLED devices will be correctly identified via brand matching Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com> --- .../service/update/ReleaseService.kt | 53 +++++++++++++++++-- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/ReleaseService.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/ReleaseService.kt index 4b15b9d2..c03221c6 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/ReleaseService.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/ReleaseService.kt @@ -18,12 +18,59 @@ import javax.inject.Inject private const val TAG = "updateService" private const val DEFAULT_REPO = "wled/WLED" +enum class UpdateSourceType { + OFFICIAL_WLED, QUINLED, CUSTOM +} + +data class UpdateSourceDefinition( + val type: UpdateSourceType, + val brandPattern: String, + val githubOwner: String, + val githubRepo: String +) + +object UpdateSourceRegistry { + val sources = listOf( + UpdateSourceDefinition( + type = UpdateSourceType.OFFICIAL_WLED, + brandPattern = "WLED", + githubOwner = "wled", + githubRepo = "WLED" + ), UpdateSourceDefinition( + type = UpdateSourceType.QUINLED, + brandPattern = "QuinLED", + githubOwner = "intermittech", + githubRepo = "QuinLED-Firmware" + ) + ) + + fun getSource(info: Info): UpdateSourceDefinition? { + return sources.find { + info.brand == it.brandPattern + } + } +} + /** - * Extracts repository from device info. - * Uses the repo field if available (format: "owner/name"), otherwise defaults to "wled/WLED" + * Extracts repository from device info using a three-tier fallback strategy: + * 1. First: Use the repo field if available (format: "owner/name") - added in WLED 0.15.2 + * 2. Second: Use UpdateSourceRegistry based on brand pattern matching + * 3. Third: Default to "wled/WLED" */ fun getRepositoryFromInfo(info: Info): String { - return info.repo ?: DEFAULT_REPO + // First priority: Use the repo field if present (WLED 0.15.2+) + if (!info.repo.isNullOrBlank()) { + return info.repo + } + + // Second priority: Use brand-based registry lookup + val source = UpdateSourceRegistry.getSource(info) + if (source != null) { + return "${source.githubOwner}/${source.githubRepo}" + } + + // Final fallback: Default repository + return DEFAULT_REPO } /** From b89efb4c2e987ba5b87735ad88fcfc5571e03d53 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 16:02:29 +0000 Subject: [PATCH 06/20] Fix checkForUpdates to refresh all discovered device repositories - Inject WebsocketClient map into DeviceEditViewModel - Update checkForUpdates() to collect repositories from all connected devices - Now matches MainViewModel's approach: default + unique list of repos - Ensures updates are fetched for all device types (QuinLED, MoonModules, etc.) Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com> --- .../deviceEdit/DeviceEditViewModel.kt | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/deviceEdit/DeviceEditViewModel.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/deviceEdit/DeviceEditViewModel.kt index a765c8c9..3dc831dd 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/deviceEdit/DeviceEditViewModel.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/deviceEdit/DeviceEditViewModel.kt @@ -11,6 +11,8 @@ import ca.cgagnier.wlednativeandroid.repository.DeviceRepository import ca.cgagnier.wlednativeandroid.repository.VersionWithAssetsRepository import ca.cgagnier.wlednativeandroid.service.api.github.GithubApi import ca.cgagnier.wlednativeandroid.service.update.ReleaseService +import ca.cgagnier.wlednativeandroid.service.update.getRepositoryFromInfo +import ca.cgagnier.wlednativeandroid.service.websocket.WebsocketClient import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -19,12 +21,14 @@ import kotlinx.coroutines.launch import javax.inject.Inject const val TAG = "DeviceEditViewModel" +private const val DEFAULT_REPO = "wled/WLED" @HiltViewModel class DeviceEditViewModel @Inject constructor( private val repository: DeviceRepository, private val versionWithAssetsRepository: VersionWithAssetsRepository, - private val githubApi: GithubApi + private val githubApi: GithubApi, + private val websocketClients: Map ) : ViewModel() { private var _updateDetailsVersion: MutableStateFlow = MutableStateFlow(null) @@ -112,8 +116,21 @@ class DeviceEditViewModel @Inject constructor( repository.update(updatedDevice) try { val releaseService = ReleaseService(versionWithAssetsRepository) - // Always include the default repository - val repositories = setOf("wled/WLED") + + // Collect unique repositories from all connected devices + val repositories = mutableSetOf() + repositories.add(DEFAULT_REPO) // Always include the default WLED repository + + websocketClients.values.forEach { client -> + val info = client.deviceState.stateInfo.value?.info + if (info != null) { + val repo = getRepositoryFromInfo(info) + repositories.add(repo) + Log.d(TAG, "Found device using repository: $repo") + } + } + + Log.i(TAG, "Refreshing versions from ${repositories.size} repositories: $repositories") releaseService.refreshVersions(githubApi, repositories) } finally { _isCheckingUpdates.value = false From cd0ce47128b6bdf9dbcda987c389d3e1cdfe32dd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 16:10:15 +0000 Subject: [PATCH 07/20] Fix checkForUpdates to only refresh selected device's repository - Changed from collecting all device repositories to just the selected device - Lookup device by macAddress in websocketClients map - Only refresh that specific device's repository plus default fallback - More appropriate for device-specific edit context - MainViewModel still handles refreshing all repositories globally Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com> --- .../deviceEdit/DeviceEditViewModel.kt | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/deviceEdit/DeviceEditViewModel.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/deviceEdit/DeviceEditViewModel.kt index 3dc831dd..678b1f3e 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/deviceEdit/DeviceEditViewModel.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/deviceEdit/DeviceEditViewModel.kt @@ -117,17 +117,19 @@ class DeviceEditViewModel @Inject constructor( try { val releaseService = ReleaseService(versionWithAssetsRepository) - // Collect unique repositories from all connected devices + // Get repository for this specific device val repositories = mutableSetOf() repositories.add(DEFAULT_REPO) // Always include the default WLED repository - websocketClients.values.forEach { client -> - val info = client.deviceState.stateInfo.value?.info - if (info != null) { - val repo = getRepositoryFromInfo(info) - repositories.add(repo) - Log.d(TAG, "Found device using repository: $repo") - } + // Look up the specific device's websocket client to get its repository + val client = websocketClients[device.macAddress] + val info = client?.deviceState?.stateInfo?.value?.info + if (info != null) { + val repo = getRepositoryFromInfo(info) + repositories.add(repo) + Log.d(TAG, "Refreshing versions for device repository: $repo") + } else { + Log.d(TAG, "Device info not available, using default repository only") } Log.i(TAG, "Refreshing versions from ${repositories.size} repositories: $repositories") From a2d0b82dd2ff34effb02fb2954c682c0a7f08521 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sat, 7 Feb 2026 20:04:20 +0000 Subject: [PATCH 08/20] Update app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/ReleaseService.kt Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Signed-off-by: Will Tatam --- .../wlednativeandroid/service/update/ReleaseService.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/ReleaseService.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/ReleaseService.kt index c03221c6..d096b61e 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/ReleaseService.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/ReleaseService.kt @@ -79,11 +79,12 @@ fun getRepositoryFromInfo(info: Info): String { */ fun splitRepository(repository: String): Pair { val parts = repository.split("/") - return if (parts.size == 2) { - Pair(parts[0], parts[1]) + if (parts.size == 2 && parts[0].isNotBlank() && parts[1].isNotBlank()) { + return Pair(parts[0], parts[1]) } else { Log.w(TAG, "Invalid repo format: $repository, using default") - Pair("wled", "WLED") + val defaultParts = DEFAULT_REPO.split("/") + return Pair(defaultParts[0], defaultParts[1]) } } From 24258a2df148f9779416b966ca945a08eb54da93 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sat, 7 Feb 2026 20:05:02 +0000 Subject: [PATCH 09/20] Update app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/deviceEdit/DeviceEditViewModel.kt Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Signed-off-by: Will Tatam --- .../ui/homeScreen/deviceEdit/DeviceEditViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/deviceEdit/DeviceEditViewModel.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/deviceEdit/DeviceEditViewModel.kt index 678b1f3e..17351daa 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/deviceEdit/DeviceEditViewModel.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/deviceEdit/DeviceEditViewModel.kt @@ -75,7 +75,7 @@ class DeviceEditViewModel @Inject constructor( fun showUpdateDetails(device: Device, deviceStateInfo: DeviceStateInfo?, version: String) = viewModelScope.launch(Dispatchers.IO) { // Extract repository from device info, defaulting to "wled/WLED" - val repository = deviceStateInfo?.info?.repo ?: "wled/WLED" + val repository = deviceStateInfo?.info?.let { getRepositoryFromInfo(it) } ?: DEFAULT_REPO _updateDetailsVersion.value = versionWithAssetsRepository.getVersionByTag(repository, version) } From f04e26bfd5317de3879bcffd04dc20c3add2ba01 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 20:09:31 +0000 Subject: [PATCH 10/20] Centralize DEFAULT_REPO constant to avoid duplication - Made DEFAULT_REPO public in ReleaseService.kt - Import DEFAULT_REPO in MainViewModel and DeviceEditViewModel - Removes duplicate constant definitions - Ensures single source of truth for default repository value - Improves maintainability Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com> --- .../cgagnier/wlednativeandroid/service/update/ReleaseService.kt | 2 +- .../main/java/ca/cgagnier/wlednativeandroid/ui/MainViewModel.kt | 2 +- .../ui/homeScreen/deviceEdit/DeviceEditViewModel.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/ReleaseService.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/ReleaseService.kt index d096b61e..8288c8fb 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/ReleaseService.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/ReleaseService.kt @@ -16,7 +16,7 @@ import kotlinx.coroutines.withContext import javax.inject.Inject private const val TAG = "updateService" -private const val DEFAULT_REPO = "wled/WLED" +const val DEFAULT_REPO = "wled/WLED" enum class UpdateSourceType { OFFICIAL_WLED, QUINLED, CUSTOM diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/MainViewModel.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/MainViewModel.kt index f3681149..edef6eb9 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/MainViewModel.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/MainViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope import ca.cgagnier.wlednativeandroid.repository.DeviceRepository import ca.cgagnier.wlednativeandroid.repository.UserPreferencesRepository import ca.cgagnier.wlednativeandroid.service.api.github.GithubApi +import ca.cgagnier.wlednativeandroid.service.update.DEFAULT_REPO import ca.cgagnier.wlednativeandroid.service.update.ReleaseService import ca.cgagnier.wlednativeandroid.service.update.getRepositoryFromInfo import ca.cgagnier.wlednativeandroid.service.websocket.WebsocketClient @@ -17,7 +18,6 @@ import java.util.concurrent.TimeUnit.DAYS import javax.inject.Inject private const val TAG = "MainViewModel" -private const val DEFAULT_REPO = "wled/WLED" @HiltViewModel class MainViewModel @Inject constructor( diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/deviceEdit/DeviceEditViewModel.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/deviceEdit/DeviceEditViewModel.kt index 17351daa..bdd71138 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/deviceEdit/DeviceEditViewModel.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/deviceEdit/DeviceEditViewModel.kt @@ -10,6 +10,7 @@ import ca.cgagnier.wlednativeandroid.model.wledapi.DeviceStateInfo import ca.cgagnier.wlednativeandroid.repository.DeviceRepository import ca.cgagnier.wlednativeandroid.repository.VersionWithAssetsRepository import ca.cgagnier.wlednativeandroid.service.api.github.GithubApi +import ca.cgagnier.wlednativeandroid.service.update.DEFAULT_REPO import ca.cgagnier.wlednativeandroid.service.update.ReleaseService import ca.cgagnier.wlednativeandroid.service.update.getRepositoryFromInfo import ca.cgagnier.wlednativeandroid.service.websocket.WebsocketClient @@ -21,7 +22,6 @@ import kotlinx.coroutines.launch import javax.inject.Inject const val TAG = "DeviceEditViewModel" -private const val DEFAULT_REPO = "wled/WLED" @HiltViewModel class DeviceEditViewModel @Inject constructor( From 48a299b5e206fd92ab06bd986cca22d00ec994b5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 20:15:40 +0000 Subject: [PATCH 11/20] =?UTF-8?q?Fix=20data=20loss=20in=20database=20migra?= =?UTF-8?q?tion=209=E2=86=9210?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implement onPostMigrate() to copy data from old tables to new ones - Copy all Version records from Version_old to Version with default repository "wled/WLED" - Copy all Asset records from Asset_old to Asset with default repository "wled/WLED" - Add comprehensive logging to track migration success - Prevents data loss that would have occurred without data copying - Follows same pattern as DbMigration7To8 - Migration 10→11 will then safely delete the old tables Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com> --- .../repository/migrations/DbMigration9To10.kt | 96 ++++++++++++++++++- 1 file changed, 93 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/repository/migrations/DbMigration9To10.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/repository/migrations/DbMigration9To10.kt index 9d4c182e..611191ce 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/repository/migrations/DbMigration9To10.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/repository/migrations/DbMigration9To10.kt @@ -1,15 +1,105 @@ package ca.cgagnier.wlednativeandroid.repository.migrations +import android.util.Log import androidx.room.RenameTable import androidx.room.migration.AutoMigrationSpec +import androidx.sqlite.db.SupportSQLiteDatabase + +private const val TAG = "DbMigration9To10" /** * Migration from 9->10 adds repository information to Version and Asset tables * to support tracking releases from multiple WLED repositories/forks. * - * We rename the old tables, create new ones with repository field (defaulting to "wled/WLED"), - * then drop the old tables. This preserves existing data while adding the new repository tracking. + * We rename the old tables, create new ones with repository field, + * copy existing data with default repository "wled/WLED", then drop the old tables. */ @RenameTable(fromTableName = "Version", toTableName = "Version_old") @RenameTable(fromTableName = "Asset", toTableName = "Asset_old") -class DbMigration9To10 : AutoMigrationSpec +class DbMigration9To10 : AutoMigrationSpec { + override fun onPostMigrate(db: SupportSQLiteDatabase) { + Log.i(TAG, "onPostMigrate starting - migrating Version and Asset data") + + // Migrate Version table + val originalVersionCountCursor = db.query("SELECT COUNT(*) FROM Version_old") + var originalVersionCount = 0 + if (originalVersionCountCursor.moveToFirst()) { + originalVersionCount = originalVersionCountCursor.getInt(0) + } + originalVersionCountCursor.close() + Log.i(TAG, "Total versions in old 'Version' table: $originalVersionCount") + + // Copy data from Version_old to Version with default repository + db.execSQL( + """ + INSERT OR IGNORE INTO Version ( + tagName, + repository, + name, + description, + isPrerelease, + publishedDate, + htmlUrl + ) + SELECT + tagName, + 'wled/WLED' AS repository, + name, + description, + isPrerelease, + publishedDate, + htmlUrl + FROM Version_old + """.trimIndent() + ) + + val migratedVersionCountCursor = db.query("SELECT COUNT(*) FROM Version") + var migratedVersionCount = 0 + if (migratedVersionCountCursor.moveToFirst()) { + migratedVersionCount = migratedVersionCountCursor.getInt(0) + } + migratedVersionCountCursor.close() + Log.i(TAG, "Versions migrated to new table: $migratedVersionCount") + + // Migrate Asset table + val originalAssetCountCursor = db.query("SELECT COUNT(*) FROM Asset_old") + var originalAssetCount = 0 + if (originalAssetCountCursor.moveToFirst()) { + originalAssetCount = originalAssetCountCursor.getInt(0) + } + originalAssetCountCursor.close() + Log.i(TAG, "Total assets in old 'Asset' table: $originalAssetCount") + + // Copy data from Asset_old to Asset with default repository + db.execSQL( + """ + INSERT OR IGNORE INTO Asset ( + versionTagName, + repository, + name, + size, + downloadUrl, + assetId + ) + SELECT + versionTagName, + 'wled/WLED' AS repository, + name, + size, + downloadUrl, + assetId + FROM Asset_old + """.trimIndent() + ) + + val migratedAssetCountCursor = db.query("SELECT COUNT(*) FROM Asset") + var migratedAssetCount = 0 + if (migratedAssetCountCursor.moveToFirst()) { + migratedAssetCount = migratedAssetCountCursor.getInt(0) + } + migratedAssetCountCursor.close() + Log.i(TAG, "Assets migrated to new table: $migratedAssetCount") + + Log.i(TAG, "onPostMigrate done! Migration is complete.") + } +} From a02638169cb2a557a539cd731af2732091c56a5b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 20:20:32 +0000 Subject: [PATCH 12/20] Fix manual ReleaseService instantiation in DeviceEditViewModel - Inject ReleaseService via constructor instead of manual instantiation - Matches dependency injection pattern used in MainViewModel - Removes tight coupling and improves testability - Follows Hilt DI best practices - Addresses code review feedback from gemini-code-assist Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com> --- .../ui/homeScreen/deviceEdit/DeviceEditViewModel.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/deviceEdit/DeviceEditViewModel.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/deviceEdit/DeviceEditViewModel.kt index bdd71138..c10ccb1f 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/deviceEdit/DeviceEditViewModel.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/deviceEdit/DeviceEditViewModel.kt @@ -28,7 +28,8 @@ class DeviceEditViewModel @Inject constructor( private val repository: DeviceRepository, private val versionWithAssetsRepository: VersionWithAssetsRepository, private val githubApi: GithubApi, - private val websocketClients: Map + private val websocketClients: Map, + private val releaseService: ReleaseService ) : ViewModel() { private var _updateDetailsVersion: MutableStateFlow = MutableStateFlow(null) @@ -115,8 +116,6 @@ class DeviceEditViewModel @Inject constructor( val updatedDevice = device.copy(skipUpdateTag = "") repository.update(updatedDevice) try { - val releaseService = ReleaseService(versionWithAssetsRepository) - // Get repository for this specific device val repositories = mutableSetOf() repositories.add(DEFAULT_REPO) // Always include the default WLED repository From f5b6653ff0bc853a9692fcebe06205d51bfd924c Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sat, 21 Feb 2026 16:08:56 +0000 Subject: [PATCH 13/20] Add MoonMudules to UpdateSourceRegistry --- .../service/update/ReleaseService.kt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/ReleaseService.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/ReleaseService.kt index adfdfc77..bb914f2b 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/ReleaseService.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/ReleaseService.kt @@ -17,18 +17,26 @@ import kotlinx.coroutines.withContext private const val TAG = "updateService" enum class UpdateSourceType { - OFFICIAL_WLED, QUINLED, CUSTOM + OFFICIAL_WLED, QUINLED, CUSTOM, MOONMODULES } data class UpdateSourceDefinition( val type: UpdateSourceType, val brandPattern: String, + val product: String? = null, val githubOwner: String, val githubRepo: String ) object UpdateSourceRegistry { val sources = listOf( + UpdateSourceDefinition( + type = UpdateSourceType.MOONMODULES, // Must be first in the list to take precedence over the official WLED source for MoonModules devices + brandPattern = "WLED", + product = "MoonModules", + githubOwner = "MoonModules", + githubRepo = "WLED-MM" + ), UpdateSourceDefinition( type = UpdateSourceType.OFFICIAL_WLED, brandPattern = "WLED", @@ -44,6 +52,7 @@ object UpdateSourceRegistry { fun getSource(info: Info): UpdateSourceDefinition? { return sources.find { + (it.product == null || info.product == it.product) && info.brand == it.brandPattern } } From fb1472a483df6b7fe6a17683cd2d5ae5a1a5e680 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sat, 21 Feb 2026 16:18:50 +0000 Subject: [PATCH 14/20] Update app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/ReleaseService.kt Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Signed-off-by: Will Tatam --- .../wlednativeandroid/service/update/ReleaseService.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/ReleaseService.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/ReleaseService.kt index bb914f2b..b7b8c2a3 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/ReleaseService.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/ReleaseService.kt @@ -23,9 +23,9 @@ enum class UpdateSourceType { data class UpdateSourceDefinition( val type: UpdateSourceType, val brandPattern: String, - val product: String? = null, val githubOwner: String, - val githubRepo: String + val githubRepo: String, + val product: String? = null ) object UpdateSourceRegistry { From 3381ba2277787347487d0237905d5e9428480676 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sat, 21 Feb 2026 16:22:42 +0000 Subject: [PATCH 15/20] More robust UpdateSourceDefinition matching --- .../service/update/ReleaseService.kt | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/ReleaseService.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/ReleaseService.kt index b7b8c2a3..04163bb8 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/ReleaseService.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/ReleaseService.kt @@ -30,13 +30,6 @@ data class UpdateSourceDefinition( object UpdateSourceRegistry { val sources = listOf( - UpdateSourceDefinition( - type = UpdateSourceType.MOONMODULES, // Must be first in the list to take precedence over the official WLED source for MoonModules devices - brandPattern = "WLED", - product = "MoonModules", - githubOwner = "MoonModules", - githubRepo = "WLED-MM" - ), UpdateSourceDefinition( type = UpdateSourceType.OFFICIAL_WLED, brandPattern = "WLED", @@ -47,14 +40,20 @@ object UpdateSourceRegistry { brandPattern = "QuinLED", githubOwner = "intermittech", githubRepo = "QuinLED-Firmware" - ) + ), + UpdateSourceDefinition( + type = UpdateSourceType.MOONMODULES, + brandPattern = "WLED", + product = "MoonModules", + githubOwner = "MoonModules", + githubRepo = "WLED-MM" + ), ) fun getSource(info: Info): UpdateSourceDefinition? { - return sources.find { - (it.product == null || info.product == it.product) && - info.brand == it.brandPattern - } + val brandMatches = sources.filter { it.brandPattern == info.brand } + return brandMatches.find { it.product == info.product } + ?: brandMatches.find { it.product == null } } } From 08c88a120603c3eae3295bb210c8a275c3c1ee8f Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sat, 21 Feb 2026 18:10:19 +0000 Subject: [PATCH 16/20] Revert accidental change to agp version --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a1b08553..7e2b3ca1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.1.3" +agp = "8.13.2" composeBom = "2025.12.01" converterMoshi = "3.0.0" core = "4.6.2" From b35421148bb6f9808ebc9c9230f3e01d76d87fda Mon Sep 17 00:00:00 2001 From: Will Date: Sat, 21 Feb 2026 18:17:10 +0000 Subject: [PATCH 17/20] Fix issues with database migration --- .../11.json | 220 ++++++++++++++++++ .../wlednativeandroid/model/Version.kt | 3 +- .../repository/DevicesDatabase.kt | 10 +- .../migrations/DbMigration10To11.kt | 21 +- .../repository/migrations/DbMigration9To10.kt | 59 ++++- 5 files changed, 291 insertions(+), 22 deletions(-) create mode 100644 app/schemas/ca.cgagnier.wlednativeandroid.repository.DevicesDatabase/11.json diff --git a/app/schemas/ca.cgagnier.wlednativeandroid.repository.DevicesDatabase/11.json b/app/schemas/ca.cgagnier.wlednativeandroid.repository.DevicesDatabase/11.json new file mode 100644 index 00000000..c907dc5d --- /dev/null +++ b/app/schemas/ca.cgagnier.wlednativeandroid.repository.DevicesDatabase/11.json @@ -0,0 +1,220 @@ +{ + "formatVersion": 1, + "database": { + "version": 11, + "identityHash": "2760473c3744dbf6f299b09b0c84a05d", + "entities": [ + { + "tableName": "Device2", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`macAddress` TEXT NOT NULL, `address` TEXT NOT NULL, `isHidden` INTEGER NOT NULL, `originalName` TEXT NOT NULL DEFAULT '', `customName` TEXT NOT NULL DEFAULT '', `skipUpdateTag` TEXT NOT NULL DEFAULT '', `branch` TEXT NOT NULL DEFAULT 'UNKNOWN', `lastSeen` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`macAddress`))", + "fields": [ + { + "fieldPath": "macAddress", + "columnName": "macAddress", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isHidden", + "columnName": "isHidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originalName", + "columnName": "originalName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "customName", + "columnName": "customName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "skipUpdateTag", + "columnName": "skipUpdateTag", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "branch", + "columnName": "branch", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'UNKNOWN'" + }, + { + "fieldPath": "lastSeen", + "columnName": "lastSeen", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "macAddress" + ] + } + }, + { + "tableName": "Version", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tagName` TEXT NOT NULL, `repository` TEXT NOT NULL DEFAULT 'wled/WLED', `name` TEXT NOT NULL, `description` TEXT NOT NULL, `isPrerelease` INTEGER NOT NULL, `publishedDate` TEXT NOT NULL, `htmlUrl` TEXT NOT NULL, PRIMARY KEY(`tagName`, `repository`))", + "fields": [ + { + "fieldPath": "tagName", + "columnName": "tagName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "repository", + "columnName": "repository", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'wled/WLED'" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isPrerelease", + "columnName": "isPrerelease", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "publishedDate", + "columnName": "publishedDate", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "tagName", + "repository" + ] + } + }, + { + "tableName": "Asset", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`versionTagName` TEXT NOT NULL, `repository` TEXT NOT NULL, `name` TEXT NOT NULL, `size` INTEGER NOT NULL, `downloadUrl` TEXT NOT NULL, `assetId` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`versionTagName`, `repository`, `name`), FOREIGN KEY(`versionTagName`, `repository`) REFERENCES `Version`(`tagName`, `repository`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "versionTagName", + "columnName": "versionTagName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "repository", + "columnName": "repository", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "downloadUrl", + "columnName": "downloadUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "assetId", + "columnName": "assetId", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "versionTagName", + "repository", + "name" + ] + }, + "indices": [ + { + "name": "index_Asset_versionTagName", + "unique": false, + "columnNames": [ + "versionTagName" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Asset_versionTagName` ON `${TABLE_NAME}` (`versionTagName`)" + }, + { + "name": "index_Asset_repository", + "unique": false, + "columnNames": [ + "repository" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Asset_repository` ON `${TABLE_NAME}` (`repository`)" + } + ], + "foreignKeys": [ + { + "table": "Version", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "versionTagName", + "repository" + ], + "referencedColumns": [ + "tagName", + "repository" + ] + } + ] + } + ], + "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, '2760473c3744dbf6f299b09b0c84a05d')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/model/Version.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/model/Version.kt index 5a09a118..6bb2fe5c 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/model/Version.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/model/Version.kt @@ -1,13 +1,14 @@ package ca.cgagnier.wlednativeandroid.model +import androidx.room.ColumnInfo import androidx.room.Entity -import androidx.room.PrimaryKey @Entity( primaryKeys = ["tagName", "repository"] ) data class Version( val tagName: String, + @ColumnInfo(defaultValue = "'wled/WLED'") val repository: String, val name: String, val description: String, diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/repository/DevicesDatabase.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/repository/DevicesDatabase.kt index 46645db2..ab125823 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/repository/DevicesDatabase.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/repository/DevicesDatabase.kt @@ -11,8 +11,8 @@ import ca.cgagnier.wlednativeandroid.model.Device import ca.cgagnier.wlednativeandroid.model.Version import ca.cgagnier.wlednativeandroid.repository.migrations.DbMigration7To8 import ca.cgagnier.wlednativeandroid.repository.migrations.DbMigration8To9 -import ca.cgagnier.wlednativeandroid.repository.migrations.DbMigration9To10 -import ca.cgagnier.wlednativeandroid.repository.migrations.DbMigration10To11 +import ca.cgagnier.wlednativeandroid.repository.migrations.MIGRATION_9_10 +import ca.cgagnier.wlednativeandroid.repository.migrations.MIGRATION_10_11 @Database( entities = [ @@ -31,8 +31,6 @@ import ca.cgagnier.wlednativeandroid.repository.migrations.DbMigration10To11 AutoMigration(from = 6, to = 7), AutoMigration(from = 7, to = 8, spec = DbMigration7To8::class), AutoMigration(from = 8, to = 9, spec = DbMigration8To9::class), - AutoMigration(from = 9, to = 10, spec = DbMigration9To10::class), - AutoMigration(from = 10, to = 11, spec = DbMigration10To11::class), ] ) @TypeConverters(Converters::class) @@ -51,7 +49,9 @@ abstract class DevicesDatabase : RoomDatabase() { context.applicationContext, DevicesDatabase::class.java, "devices_database" - ).build() + ) + .addMigrations(MIGRATION_9_10, MIGRATION_10_11) + .build() INSTANCE = instance instance } diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/repository/migrations/DbMigration10To11.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/repository/migrations/DbMigration10To11.kt index b7bd19ec..5265021f 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/repository/migrations/DbMigration10To11.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/repository/migrations/DbMigration10To11.kt @@ -1,12 +1,23 @@ package ca.cgagnier.wlednativeandroid.repository.migrations -import androidx.room.DeleteTable -import androidx.room.migration.AutoMigrationSpec +import android.util.Log +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +private const val TAG = "DbMigration10To11" /** * Migration from 10->11 removes the old Version and Asset tables after data has been migrated * to the new schema with repository tracking support. */ -@DeleteTable(tableName = "Version_old") -@DeleteTable(tableName = "Asset_old") -class DbMigration10To11 : AutoMigrationSpec +val MIGRATION_10_11 = object : Migration(10, 11) { + override fun migrate(db: SupportSQLiteDatabase) { + Log.i(TAG, "Starting migration from 10 to 11") + + // Drop the old tables if they exist (cleanup from previous migration) + db.execSQL("DROP TABLE IF EXISTS Version_old") + db.execSQL("DROP TABLE IF EXISTS Asset_old") + + Log.i(TAG, "Migration from 10 to 11 complete!") + } +} diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/repository/migrations/DbMigration9To10.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/repository/migrations/DbMigration9To10.kt index 611191ce..230ca414 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/repository/migrations/DbMigration9To10.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/repository/migrations/DbMigration9To10.kt @@ -1,8 +1,7 @@ package ca.cgagnier.wlednativeandroid.repository.migrations import android.util.Log -import androidx.room.RenameTable -import androidx.room.migration.AutoMigrationSpec +import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase private const val TAG = "DbMigration9To10" @@ -14,13 +13,47 @@ private const val TAG = "DbMigration9To10" * We rename the old tables, create new ones with repository field, * copy existing data with default repository "wled/WLED", then drop the old tables. */ -@RenameTable(fromTableName = "Version", toTableName = "Version_old") -@RenameTable(fromTableName = "Asset", toTableName = "Asset_old") -class DbMigration9To10 : AutoMigrationSpec { - override fun onPostMigrate(db: SupportSQLiteDatabase) { - Log.i(TAG, "onPostMigrate starting - migrating Version and Asset data") - - // Migrate Version table +val MIGRATION_9_10 = object : Migration(9, 10) { + override fun migrate(db: SupportSQLiteDatabase) { + Log.i(TAG, "Starting migration from 9 to 10") + + // Rename old tables + db.execSQL("ALTER TABLE Version RENAME TO Version_old") + db.execSQL("ALTER TABLE Asset RENAME TO Asset_old") + + // Create new Version table with repository column + db.execSQL(""" + CREATE TABLE IF NOT EXISTS Version ( + tagName TEXT NOT NULL, + repository TEXT NOT NULL DEFAULT 'wled/WLED', + name TEXT NOT NULL, + description TEXT NOT NULL, + isPrerelease INTEGER NOT NULL, + publishedDate TEXT NOT NULL, + htmlUrl TEXT NOT NULL, + PRIMARY KEY(tagName, repository) + ) + """.trimIndent()) + + // Create new Asset table with repository column + db.execSQL(""" + CREATE TABLE IF NOT EXISTS Asset ( + versionTagName TEXT NOT NULL, + repository TEXT NOT NULL DEFAULT 'wled/WLED', + name TEXT NOT NULL, + size INTEGER NOT NULL, + downloadUrl TEXT NOT NULL, + assetId INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY(versionTagName, repository, name), + FOREIGN KEY(versionTagName, repository) REFERENCES Version(tagName, repository) ON DELETE CASCADE + ) + """.trimIndent()) + + // Create indices for Asset table + db.execSQL("CREATE INDEX IF NOT EXISTS index_Asset_versionTagName ON Asset (versionTagName)") + db.execSQL("CREATE INDEX IF NOT EXISTS index_Asset_repository ON Asset (repository)") + + // Migrate Version data val originalVersionCountCursor = db.query("SELECT COUNT(*) FROM Version_old") var originalVersionCount = 0 if (originalVersionCountCursor.moveToFirst()) { @@ -61,7 +94,7 @@ class DbMigration9To10 : AutoMigrationSpec { migratedVersionCountCursor.close() Log.i(TAG, "Versions migrated to new table: $migratedVersionCount") - // Migrate Asset table + // Migrate Asset data val originalAssetCountCursor = db.query("SELECT COUNT(*) FROM Asset_old") var originalAssetCount = 0 if (originalAssetCountCursor.moveToFirst()) { @@ -100,6 +133,10 @@ class DbMigration9To10 : AutoMigrationSpec { migratedAssetCountCursor.close() Log.i(TAG, "Assets migrated to new table: $migratedAssetCount") - Log.i(TAG, "onPostMigrate done! Migration is complete.") + // Drop old tables + db.execSQL("DROP TABLE IF EXISTS Version_old") + db.execSQL("DROP TABLE IF EXISTS Asset_old") + + Log.i(TAG, "Migration from 9 to 10 complete!") } } From fc30b52401345b8cbeb2dc05cced1e6b424b0c76 Mon Sep 17 00:00:00 2001 From: Will Date: Sat, 21 Feb 2026 18:45:11 +0000 Subject: [PATCH 18/20] Attempt at fix for "ReleaseService is being instantiated directly here. Since ReleaseService is an injectable class (annotated with @Inject), it should be provided by Hilt via the ViewModel's constructor. This improves testability and follows dependency injection best practices." Not sure if all this is needed? --- .../websocket/WebsocketClientManager.kt | 26 +++++++++++++++++++ .../wlednativeandroid/ui/MainViewModel.kt | 6 ++--- .../deviceEdit/DeviceEditViewModel.kt | 6 ++--- .../list/DeviceWebsocketListViewModel.kt | 6 ++++- 4 files changed, 37 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/ca/cgagnier/wlednativeandroid/service/websocket/WebsocketClientManager.kt diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/service/websocket/WebsocketClientManager.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/service/websocket/WebsocketClientManager.kt new file mode 100644 index 00000000..17bc3d30 --- /dev/null +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/service/websocket/WebsocketClientManager.kt @@ -0,0 +1,26 @@ +package ca.cgagnier.wlednativeandroid.service.websocket + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Manages the active WebSocket client instances. + * This is a singleton that provides access to all active websocket clients. + */ +@Singleton +class WebsocketClientManager @Inject constructor() { + private val _clients = MutableStateFlow>(emptyMap()) + val clients: StateFlow> = _clients.asStateFlow() + + fun updateClients(newClients: Map) { + _clients.value = newClients + } + + fun getClients(): Map { + return _clients.value + } +} + diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/MainViewModel.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/MainViewModel.kt index edef6eb9..ac6854f4 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/MainViewModel.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/MainViewModel.kt @@ -9,7 +9,7 @@ import ca.cgagnier.wlednativeandroid.service.api.github.GithubApi import ca.cgagnier.wlednativeandroid.service.update.DEFAULT_REPO import ca.cgagnier.wlednativeandroid.service.update.ReleaseService import ca.cgagnier.wlednativeandroid.service.update.getRepositoryFromInfo -import ca.cgagnier.wlednativeandroid.service.websocket.WebsocketClient +import ca.cgagnier.wlednativeandroid.service.websocket.WebsocketClientManager import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first @@ -25,7 +25,7 @@ class MainViewModel @Inject constructor( private val releaseService: ReleaseService, private val githubApi: GithubApi, private val deviceRepository: DeviceRepository, - private val websocketClients: Map + private val websocketClientManager: WebsocketClientManager ) : ViewModel() { fun downloadUpdateMetadata() { @@ -41,7 +41,7 @@ class MainViewModel @Inject constructor( val repositories = mutableSetOf() repositories.add(DEFAULT_REPO) // Always include the default WLED repository - websocketClients.values.forEach { client -> + websocketClientManager.getClients().values.forEach { client -> val info = client.deviceState.stateInfo.value?.info if (info != null) { val repo = getRepositoryFromInfo(info) diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/deviceEdit/DeviceEditViewModel.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/deviceEdit/DeviceEditViewModel.kt index c10ccb1f..b0fe5d85 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/deviceEdit/DeviceEditViewModel.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/deviceEdit/DeviceEditViewModel.kt @@ -13,7 +13,7 @@ import ca.cgagnier.wlednativeandroid.service.api.github.GithubApi import ca.cgagnier.wlednativeandroid.service.update.DEFAULT_REPO import ca.cgagnier.wlednativeandroid.service.update.ReleaseService import ca.cgagnier.wlednativeandroid.service.update.getRepositoryFromInfo -import ca.cgagnier.wlednativeandroid.service.websocket.WebsocketClient +import ca.cgagnier.wlednativeandroid.service.websocket.WebsocketClientManager import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -28,7 +28,7 @@ class DeviceEditViewModel @Inject constructor( private val repository: DeviceRepository, private val versionWithAssetsRepository: VersionWithAssetsRepository, private val githubApi: GithubApi, - private val websocketClients: Map, + private val websocketClientManager: WebsocketClientManager, private val releaseService: ReleaseService ) : ViewModel() { @@ -121,7 +121,7 @@ class DeviceEditViewModel @Inject constructor( repositories.add(DEFAULT_REPO) // Always include the default WLED repository // Look up the specific device's websocket client to get its repository - val client = websocketClients[device.macAddress] + val client = websocketClientManager.getClients()[device.macAddress] val info = client?.deviceState?.stateInfo?.value?.info if (info != null) { val repo = getRepositoryFromInfo(info) diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/list/DeviceWebsocketListViewModel.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/list/DeviceWebsocketListViewModel.kt index a70a8dde..549aaf00 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/list/DeviceWebsocketListViewModel.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/ui/homeScreen/list/DeviceWebsocketListViewModel.kt @@ -13,6 +13,7 @@ import ca.cgagnier.wlednativeandroid.repository.UserPreferencesRepository import ca.cgagnier.wlednativeandroid.service.update.DeviceUpdateManager import ca.cgagnier.wlednativeandroid.service.websocket.DeviceWithState import ca.cgagnier.wlednativeandroid.service.websocket.WebsocketClient +import ca.cgagnier.wlednativeandroid.service.websocket.WebsocketClientManager import com.squareup.moshi.Moshi import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers @@ -35,7 +36,8 @@ class DeviceWebsocketListViewModel @Inject constructor( private val deviceRepository: DeviceRepository, private val deviceUpdateManager: DeviceUpdateManager, private val okHttpClient: OkHttpClient, - private val moshi: Moshi + private val moshi: Moshi, + private val websocketClientManager: WebsocketClientManager ) : ViewModel(), DefaultLifecycleObserver { private val activeClients = MutableStateFlow>(emptyMap()) private val devicesFromDb = deviceRepository.allDevices @@ -106,6 +108,8 @@ class DeviceWebsocketListViewModel @Inject constructor( }.flowOn(Dispatchers.IO).collect { updatedClients -> // Emit the new map of clients to the StateFlow. activeClients.value = updatedClients + // Update the manager so other components can access the clients + websocketClientManager.updateClients(updatedClients) } } From 0d03fad2a956257d06cfa6417307ca0b7cc0445e Mon Sep 17 00:00:00 2001 From: Will Date: Sat, 21 Feb 2026 19:13:32 +0000 Subject: [PATCH 19/20] Handle forks using different names than WLED for their binaries --- .../service/update/DeviceUpdateService.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/DeviceUpdateService.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/DeviceUpdateService.kt index 109d3e4a..7ebf49da 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/DeviceUpdateService.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/DeviceUpdateService.kt @@ -56,7 +56,7 @@ class DeviceUpdateService( val versionWithRelease = if (combined.startsWith("v", ignoreCase = true)) combined.drop(1) else combined assetName = "WLED_${versionWithRelease}.bin" - return findAsset(assetName) + return findAsset(versionWithRelease) } /** @@ -77,13 +77,14 @@ class DeviceUpdateService( val versionWithPlatform = if (combined.startsWith("v", ignoreCase = true)) combined.drop(1) else combined assetName = "WLED_${versionWithPlatform}.bin" - return findAsset(assetName) + return findAsset(versionWithPlatform) } private fun findAsset(assetName: String): Boolean { for (asset in versionWithAssets.assets) { - if (asset.name == assetName) { + if (asset.name.endsWith("${assetName}.bin")) { this.asset = asset + this.assetName = asset.name couldDetermineAsset = true return true } From d15c838eadeb33c3fbe2ad79a9c51b384dc6f35b Mon Sep 17 00:00:00 2001 From: Will Date: Sat, 21 Feb 2026 19:15:17 +0000 Subject: [PATCH 20/20] Prefer UpdateSourceRegistry over repo field pending checking of accuracy of the repo field --- .../service/update/ReleaseService.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/ReleaseService.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/ReleaseService.kt index 3efbc265..41e697d5 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/ReleaseService.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/service/update/ReleaseService.kt @@ -66,16 +66,18 @@ object UpdateSourceRegistry { * 3. Third: Default to "wled/WLED" */ fun getRepositoryFromInfo(info: Info): String { - // First priority: Use the repo field if present (WLED 0.15.2+) - if (!info.repo.isNullOrBlank()) { - return info.repo - } - + + // Second priority: Use brand-based registry lookup val source = UpdateSourceRegistry.getSource(info) if (source != null) { return "${source.githubOwner}/${source.githubRepo}" } + + // TODO: Should be first, but possible bad MoonModules build with the wrong repo - TBC + if (!info.repo.isNullOrBlank()) { + return info.repo + } // Final fallback: Default repository return DEFAULT_REPO