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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] =?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/12] 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