@@ -7,6 +7,8 @@ import android.provider.Settings
77import com.nendo.argosy.BuildConfig
88import com.nendo.argosy.data.preferences.UserPreferencesRepository
99import com.nendo.argosy.data.repository.BiosRepository
10+ import com.nendo.argosy.data.sync.AccountRemovalResult
11+ import com.nendo.argosy.data.sync.UnflushedQueuePolicy
1012import android.net.ConnectivityManager
1113import android.net.Network
1214import com.nendo.argosy.util.Logger
@@ -81,10 +83,11 @@ class RomMConnectionManager @Inject constructor(
8183 @ApplicationContext private val context : Context ,
8284 private val userPreferencesRepository : UserPreferencesRepository ,
8385 private val saveSyncRepository : dagger.Lazy <com.nendo.argosy.data.repository.SaveSyncRepository >,
84- private val databaseAdminRepository : dagger.Lazy <com.nendo.argosy.data.repository.DatabaseAdminRepository >,
85- private val saveCacheRepository : dagger.Lazy <com.nendo.argosy.data.repository.SaveCacheRepository >,
8686 private val biosRepository : BiosRepository ,
8787 private val rommAccountRepository : dagger.Lazy <com.nendo.argosy.data.repository.RomMAccountRepository >,
88+ private val accountRemovalService : dagger.Lazy <com.nendo.argosy.data.sync.AccountRemovalService >,
89+ private val syncCoordinator : dagger.Lazy <com.nendo.argosy.data.sync.SyncCoordinator >,
90+ private val retroAchievementsRepository : dagger.Lazy <com.nendo.argosy.data.repository.RetroAchievementsRepository >,
8891 private val apiFactory : RomMApiFactory
8992) {
9093 private var api: RomMApi ? = null
@@ -200,38 +203,39 @@ class RomMConnectionManager @Inject constructor(
200203 })
201204 }
202205
203- private fun normalizeServerKey (url : String ): String =
204- url.trim().lowercase().removePrefix(" https://" ).removePrefix(" http://" ).trimEnd(' /' )
205-
206206 private suspend fun fetchCurrentUser (target : RomMApi ): RomMUser ? = try {
207207 val response = target.getCurrentUser()
208208 if (response.isSuccessful) response.body() else null
209209 } catch (_: Exception ) {
210210 null
211211 }
212212
213- private suspend fun persistRommCredentials (newBaseUrl : String , token : String , user : RomMUser ? ) {
214- val stored = userPreferencesRepository.preferences.first()
215- val storedKey = stored.rommBaseUrl?.let { normalizeServerKey(it) }
216- val newKey = normalizeServerKey(newBaseUrl)
217- val storedUserId = stored.rommUserId
213+ private fun normalizeServerKey (url : String ): String =
214+ url.trim().lowercase().removePrefix(" https://" ).removePrefix(" http://" ).trimEnd(' /' )
218215
219- val serverChanged = ! storedKey.isNullOrBlank() && storedKey != newKey
220- val userChanged = storedUserId != null && user != null && storedUserId != user.id
216+ /* *
217+ * Refuses a sign-in that would put a second server's library alongside the first.
218+ *
219+ * Rom ids are only unique within one RomM instance, so two servers on one device collide on
220+ * every id the library, saves and sync queues are keyed by. Accounts are the supported way to
221+ * hold more than one identity, and they share the server the device is already registered to.
222+ * Nothing is deleted here; the existing library stays exactly as it is and the sign-in simply
223+ * does not happen.
224+ */
225+ private suspend fun requireSameServer (newBaseUrl : String ) {
226+ val accounts = rommAccountRepository.get().accounts()
227+ val known = accounts.map { normalizeServerKey(it.baseUrl) }.filter { it.isNotBlank() }.toSet()
228+ if (known.isEmpty()) return
229+ val newKey = normalizeServerKey(newBaseUrl)
230+ if (newKey in known) return
231+ Logger .info(TAG , " persistRommCredentials: refused sign-in to $newKey , device is registered to ${known.joinToString()} " )
232+ throw IllegalStateException (
233+ " This device is already signed in to a different RomM server. Remove the existing accounts before connecting to another server."
234+ )
235+ }
221236
222- if (serverChanged || userChanged) {
223- val pendingUploads = saveCacheRepository.get().getPendingSyncCounts().pendingUploads
224- if (pendingUploads > 0 ) {
225- Logger .info(TAG , " persistRommCredentials: identity switch blocked, $pendingUploads saves pending upload" )
226- throw IllegalStateException (
227- " Sync saves first - $pendingUploads pending upload. Switching accounts would delete them."
228- )
229- }
230- val reason = if (serverChanged) " server changed ($storedKey -> $newKey )"
231- else " user changed ($storedUserId -> ${user?.id} )"
232- Logger .info(TAG , " persistRommCredentials: $reason , purging RomM library" )
233- databaseAdminRepository.get().purgeRomMLibrary()
234- }
237+ private suspend fun persistRommCredentials (newBaseUrl : String , token : String , user : RomMUser ? ) {
238+ requireSameServer(newBaseUrl)
235239 userPreferencesRepository.setRomMCredentials(newBaseUrl, token, user?.username, user?.id)
236240 if (user != null ) {
237241 val stored = userPreferencesRepository.preferences.first()
@@ -548,15 +552,42 @@ class RomMConnectionManager @Inject constructor(
548552 }
549553
550554 /* *
551- * Forgets the stored RomM identity and tears down the live session. Library rows and
552- * downloaded content are left alone; signing back in as the same user reuses them, and
553- * signing in as a different user purges via [persistRommCredentials].
555+ * Signs the active account out: its own rows, cached saves, preferences and credentials go,
556+ * and nothing another account or the shared library owns is touched.
557+ *
558+ * This is removal aimed at the account that happens to be live, so it runs through the same
559+ * service rather than a second, weaker path - that is what brings the switch-in-progress
560+ * guard and the unflushed-work policy with it.
561+ *
562+ * The queue is drained first, while the token still authenticates. Everything the account
563+ * cached and never sent can only be sent as that account, so an upload deferred past sign-out
564+ * is an upload that never happens; refusing here is what keeps the local copy that is still
565+ * the only copy. [discardUnflushed] gives up on whatever the drain could not deliver.
554566 */
555- suspend fun signOut () {
567+ suspend fun signOut (discardUnflushed : Boolean = false): AccountRemovalResult {
568+ val active = rommAccountRepository.get().activeAccount()
569+ ? : run {
570+ disconnect()
571+ userPreferencesRepository.clearRomMCredentials()
572+ return AccountRemovalResult .UnknownAccount
573+ }
574+ val drained = syncCoordinator.get().processQueue()
575+ Logger .info(TAG , " signOut: drained queued work before removal, result=$drained " )
576+ val policy = if (discardUnflushed) {
577+ UnflushedQueuePolicy .DISCARD
578+ } else {
579+ UnflushedQueuePolicy .REFUSE
580+ }
581+ val result = accountRemovalService.get().remove(active.id, policy)
582+ if (result is AccountRemovalResult .Refused || result is AccountRemovalResult .SwitchInProgress ) {
583+ Logger .info(TAG , " signOut: not signed out, $result " )
584+ return result
585+ }
556586 disconnect()
557- rommAccountRepository.get().activeAccount()?.let { rommAccountRepository.get().forget(it.id) }
558587 userPreferencesRepository.clearRomMCredentials()
559- Logger .info(TAG , " signOut: cleared stored RomM identity" )
588+ retroAchievementsRepository.get().syncRetroArchCredentials()
589+ Logger .info(TAG , " signOut: removed user ${active.rommUserId} and cleared the stored identity" )
590+ return result
560591 }
561592
562593 /* *
0 commit comments