diff --git a/.claude/settings.local.json b/.claude/settings.local.json index a2f4871d..a59c4252 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -552,7 +552,8 @@ "Bash(\"/c/Users/dong/go/bin/go1.22.10.exe\" install golang.org/x/mobile/cmd/gomobile@c31d5b91ecc32c0d598b8fe8457d244ca0b4e815)", "Bash(\"/c/Users/dong/go/bin/go1.22.10.exe\" install golang.org/x/mobile/cmd/gobind@c31d5b91ecc32c0d598b8fe8457d244ca0b4e815)", "Bash(\"/c/Users/dong/go/bin/go1.22.10.exe\" mod tidy)", - "Bash(adb devices:*)" + "Bash(adb devices:*)", + "Bash(adb logcat:*)" ], "deny": [], "ask": [] diff --git a/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/data/local/Database.kt b/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/data/local/Database.kt index 45f0331a..d6b5f6c0 100644 --- a/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/data/local/Database.kt +++ b/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/data/local/Database.kt @@ -66,14 +66,39 @@ interface ShareRecordDao { suspend fun getShareCount(): Int } +/** + * Entity for storing app settings (like persistent partyId) + */ +@Entity(tableName = "app_settings") +data class AppSettingEntity( + @PrimaryKey + val key: String, + + @ColumnInfo(name = "value") + val value: String +) + +/** + * DAO for app settings + */ +@Dao +interface AppSettingDao { + @Query("SELECT value FROM app_settings WHERE `key` = :key") + suspend fun getValue(key: String): String? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun setValue(setting: AppSettingEntity) +} + /** * Room database */ @Database( - entities = [ShareRecordEntity::class], - version = 1, + entities = [ShareRecordEntity::class, AppSettingEntity::class], + version = 2, exportSchema = false ) abstract class TssDatabase : RoomDatabase() { abstract fun shareRecordDao(): ShareRecordDao + abstract fun appSettingDao(): AppSettingDao } diff --git a/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/data/repository/TssRepository.kt b/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/data/repository/TssRepository.kt index 79b2e0ec..c1516ab7 100644 --- a/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/data/repository/TssRepository.kt +++ b/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/data/repository/TssRepository.kt @@ -1,6 +1,8 @@ package com.durian.tssparty.data.repository import android.util.Base64 +import com.durian.tssparty.data.local.AppSettingDao +import com.durian.tssparty.data.local.AppSettingEntity import com.durian.tssparty.data.local.ShareRecordDao import com.durian.tssparty.data.local.ShareRecordEntity import com.durian.tssparty.data.local.TssNativeBridge @@ -28,7 +30,8 @@ import javax.inject.Singleton class TssRepository @Inject constructor( private val grpcClient: GrpcClient, private val tssNativeBridge: TssNativeBridge, - private val shareRecordDao: ShareRecordDao + private val shareRecordDao: ShareRecordDao, + private val appSettingDao: AppSettingDao ) { private val _currentSession = MutableStateFlow(null) val currentSession: StateFlow = _currentSession.asStateFlow() @@ -42,7 +45,9 @@ class TssRepository @Inject constructor( // Expose gRPC connection events for UI notifications val grpcConnectionEvents: SharedFlow = grpcClient.connectionEvents - private var partyId: String = UUID.randomUUID().toString() + // partyId is loaded once from database in registerParty() and cached here + // This matches Electron's getOrCreatePartyId() pattern + private lateinit var partyId: String private var messageCollectionJob: Job? = null private var sessionEventJob: Job? = null @@ -56,8 +61,14 @@ class TssRepository @Inject constructor( /** * Get the current party ID + * Note: This will throw UninitializedPropertyAccessException if called before registerParty() */ - fun getPartyId(): String = partyId + fun getPartyId(): String { + if (!::partyId.isInitialized) { + throw IllegalStateException("partyId not initialized. Call registerParty() first.") + } + return partyId + } // Track current message routing params for reconnection recovery private var currentMessageRoutingSessionId: String? = null @@ -146,8 +157,23 @@ class TssRepository @Inject constructor( /** * Register this party with the router * Also subscribes to session events (matching Electron behavior) + * + * This method loads or creates a persistent partyId from the database (matching Electron's + * getOrCreatePartyId pattern). The partyId is loaded ONCE here and cached in memory. */ suspend fun registerParty(): String { + // Load or create persistent partyId (matching Electron's getOrCreatePartyId) + val existingPartyId = appSettingDao.getValue("party_id") + partyId = if (existingPartyId != null) { + android.util.Log.d("TssRepository", "Loaded existing partyId: $existingPartyId") + existingPartyId + } else { + val newPartyId = UUID.randomUUID().toString() + appSettingDao.setValue(AppSettingEntity("party_id", newPartyId)) + android.util.Log.d("TssRepository", "Generated new partyId: $newPartyId") + newPartyId + } + grpcClient.registerParty(partyId, "temporary", "1.0.0") // Subscribe to session events immediately after registration (like Electron does) diff --git a/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/di/AppModule.kt b/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/di/AppModule.kt index c3652dd4..b62a4262 100644 --- a/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/di/AppModule.kt +++ b/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/di/AppModule.kt @@ -2,6 +2,9 @@ package com.durian.tssparty.di import android.content.Context import androidx.room.Room +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import com.durian.tssparty.data.local.AppSettingDao import com.durian.tssparty.data.local.ShareRecordDao import com.durian.tssparty.data.local.TssDatabase import com.durian.tssparty.data.local.TssNativeBridge @@ -20,6 +23,17 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) object AppModule { + // Migration from version 1 to 2: add app_settings table + private val MIGRATION_1_2 = object : Migration(1, 2) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL( + "CREATE TABLE IF NOT EXISTS `app_settings` (" + + "`key` TEXT NOT NULL PRIMARY KEY, " + + "`value` TEXT NOT NULL)" + ) + } + } + @Provides @Singleton fun provideGson(): Gson { @@ -33,7 +47,9 @@ object AppModule { context, TssDatabase::class.java, "tss_party.db" - ).build() + ) + .addMigrations(MIGRATION_1_2) + .build() } @Provides @@ -42,6 +58,12 @@ object AppModule { return database.shareRecordDao() } + @Provides + @Singleton + fun provideAppSettingDao(database: TssDatabase): AppSettingDao { + return database.appSettingDao() + } + @Provides @Singleton fun provideGrpcClient(): GrpcClient { @@ -59,8 +81,9 @@ object AppModule { fun provideTssRepository( grpcClient: GrpcClient, tssNativeBridge: TssNativeBridge, - shareRecordDao: ShareRecordDao + shareRecordDao: ShareRecordDao, + appSettingDao: AppSettingDao ): TssRepository { - return TssRepository(grpcClient, tssNativeBridge, shareRecordDao) + return TssRepository(grpcClient, tssNativeBridge, shareRecordDao, appSettingDao) } }