Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ abstract class PendingDatabaseImp : RoomDatabase(), PendingDatabase {
if (INSTANCE != null && currentIdentityNumber != scopedIdentity) {
INSTANCE?.close()
INSTANCE = null
supportSQLiteDatabase = null
}
if (INSTANCE == null) {
val dbPath = File(dbDir(context, scopedIdentity), PENDING_DB_NAME).absolutePath
Expand Down Expand Up @@ -188,7 +189,10 @@ abstract class PendingDatabaseImp : RoomDatabase(), PendingDatabase {

override fun close() {
super.close()
INSTANCE = null
currentIdentityNumber = null
synchronized(lock) {
INSTANCE = null
currentIdentityNumber = null
supportSQLiteDatabase = null
}
}
}
9 changes: 5 additions & 4 deletions app/src/main/java/one/mixin/android/di/BaseDbModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,23 @@ import one.mixin.android.db.WalletDatabase
import one.mixin.android.db.pending.PendingDatabase
import one.mixin.android.fts.FtsDatabase
import one.mixin.android.session.CurrentUserScopeManager
import javax.inject.Provider

@InstallIn(SingletonComponent::class)
@Module
internal object BaseDbModule {
@Provides
fun provideSignalDb(app: Application) = SignalDatabase.getDatabase(app)
@Provides
fun provideFtsDb(scopeManager: CurrentUserScopeManager) = scopeManager.getFtsDatabase()
fun provideFtsDb(scopeManagerProvider: Provider<CurrentUserScopeManager>) = scopeManagerProvider.get().getFtsDatabase()
@Provides
fun provideWalletDatabase(scopeManager: CurrentUserScopeManager) = scopeManager.getWalletDatabase()
fun provideWalletDatabase(scopeManagerProvider: Provider<CurrentUserScopeManager>) = scopeManagerProvider.get().getWalletDatabase()
@Provides
fun provideRatchetSenderKeyDao(db: SignalDatabase) = db.ratchetSenderKeyDao()
@Provides
fun provideDb(scopeManager: CurrentUserScopeManager) = scopeManager.getMixinDatabase()
fun provideDb(scopeManagerProvider: Provider<CurrentUserScopeManager>) = scopeManagerProvider.get().getMixinDatabase()
@Provides
fun providePendingDatabase(scopeManager: CurrentUserScopeManager): PendingDatabase = scopeManager.getPendingDatabase()
fun providePendingDatabase(scopeManagerProvider: Provider<CurrentUserScopeManager>): PendingDatabase = scopeManagerProvider.get().getPendingDatabase()
@Provides
fun provideUserDao(db: MixinDatabase) = db.userDao()
@Provides
Expand Down
10 changes: 9 additions & 1 deletion app/src/main/java/one/mixin/android/job/Injector.kt
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,15 @@ open class Injector {
}

init {
injectSelf()
// Defer session-scoped injection until ensureSessionInjection() is called
// This avoids crashes when Session.getAccount() is null at construction time
try {
if (Session.getAccount() != null) {
injectSelf()
}
} catch (e: Exception) {
// Injection will happen later via ensureSessionInjection()
}
}

protected tailrec fun signalKeysChannel(blazeMessage: BlazeMessage): JsonElement? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import one.mixin.android.session.Session
import one.mixin.android.vo.SafeBox
import one.mixin.android.vo.route.serializer.SafeBoxSerializer
Expand All @@ -21,16 +22,45 @@ class SafeBoxStoreManager
constructor(
@ApplicationContext private val appContext: Context,
) {
private val stores = ConcurrentHashMap<String, DataStore<SafeBox>>()
private data class StoreEntry(
val store: DataStore<SafeBox>,
val scope: CoroutineScope,
)

private val stores = ConcurrentHashMap<String, StoreEntry>()

fun current(): DataStore<SafeBox> {
val accountId = Session.getAccountId() ?: "temp"
return stores.getOrPut(accountId) {
DataStoreFactory.create(
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
val store = DataStoreFactory.create(
serializer = SafeBoxSerializer,
produceFile = { appContext.dataStoreFile("safe_box_$accountId.store") },
scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
scope = scope,
)
StoreEntry(store, scope)
}.store
}

/**
* Clear the DataStore for the current account.
* This cancels the associated coroutine scope and removes the store from the cache.
*/
fun clearCurrent() {
val accountId = Session.getAccountId() ?: return
stores.remove(accountId)?.let { entry ->
entry.scope.cancel()
}
}

/**
* Clear all DataStores and cancel all associated coroutine scopes.
* This should be called on logout or when switching accounts.
*/
fun clearAll() {
stores.values.forEach { entry ->
entry.scope.cancel()
}
stores.clear()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,32 @@ suspend fun initializeAccountSession(
account: Account,
sessionKey: EdKeyPair,
) {
withContext(Dispatchers.IO) {
clearJobsAndRawTransaction(context, account.identityNumber)
}

CryptoWalletHelper.clear(context)
context.defaultSharedPreferences.clear()
// Check if we're logging into the same account to preserve user settings
val isSameUser = Session.getAccountId() == account.userId

// Store session data first
val privateKey = sessionKey.privateKey
val pinToken = decryptPinToken(account.pinToken.decodeBase64(), privateKey)
Session.storeEd25519Seed(privateKey.base64Encode())
Session.storePinToken(pinToken.base64Encode())
Session.storeAccount(account)

// Enter the user scope and migrate databases BEFORE clearing anything
// This ensures we operate on the correct scoped database after migration
resolveCurrentUserScopeManager(context).enter(account)

// Now clear jobs and raw transactions from the migrated scoped database
withContext(Dispatchers.IO) {
clearJobsAndRawTransaction(context, account.identityNumber)
}

// Only clear crypto wallet and shared preferences if switching to a different user
// This preserves user settings on same-account re-login
if (!isSameUser) {
CryptoWalletHelper.clear(context)
context.defaultSharedPreferences.clear()
}

if (Session.hasPhone()) {
removeValueFromEncryptedPreferences(context, Constants.Tip.MNEMONIC)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ import one.mixin.android.Constants
import one.mixin.android.R
import one.mixin.android.compose.theme.MixinAppTheme
import one.mixin.android.extension.openUrl
import one.mixin.android.extension.toast
import one.mixin.android.ui.landing.SetupPinViewModel
import one.mixin.android.ui.landing.vo.SetupState
import one.mixin.android.ui.tip.LegacyPIN
Expand All @@ -54,7 +53,6 @@ fun SetPinLoadingPage(
val coroutineScope = rememberCoroutineScope()
val setupState by viewModel.setupState.observeAsState(SetupState.Loading)
val tipStep by viewModel.tipStep.observeAsState(TryConnecting)
val errorMessage by viewModel.errorMessage.observeAsState("")
val context = LocalContext.current

LaunchedEffect(pin) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,16 @@ suspend fun clearJobsAndRawTransaction(
val supportsDeferForeignKeys = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
val scopedDbFile = databaseFile(context, identityNumber)
val legacyDbFile = legacyDatabaseFile(context)

// Check if any database exists before proceeding
if (!scopedDbFile.exists() && !legacyDbFile.exists()) {
return
}

var db: SupportSQLiteDatabase? = null
try {
// Init database
MixinDatabase.getDatabase(context, identityNumber)
// At this point, migration should have already happened via CurrentUserScopeManager.enter()
// So we can safely get the database which should be the migrated scoped database
db = MixinDatabase.getWritableDatabase() ?: return
if (!supportsDeferForeignKeys) {
db.execSQL("PRAGMA foreign_keys = FALSE")
Expand Down