feat(android): add persistent partyId storage matching Electron behavior

- Add AppSettingEntity and AppSettingDao to Database.kt for key-value storage
- Add database migration (version 1 → 2) to create app_settings table
- Modify TssRepository.registerParty() to load/create partyId from database
- PartyId is now persisted across app restarts, matching Electron's getOrCreatePartyId()

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-01 09:50:30 -08:00
parent e2451874ea
commit b7fc488dcf
4 changed files with 84 additions and 9 deletions

View File

@ -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": []

View File

@ -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
}

View File

@ -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<TssSession?>(null)
val currentSession: StateFlow<TssSession?> = _currentSession.asStateFlow()
@ -42,7 +45,9 @@ class TssRepository @Inject constructor(
// Expose gRPC connection events for UI notifications
val grpcConnectionEvents: SharedFlow<GrpcConnectionEvent> = 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)

View File

@ -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)
}
}