fixing sign out not resetting database or inmemory caches

This commit is contained in:
Adam Brown 2022-03-28 22:12:48 +01:00
parent 0b65fd7766
commit 91cf19baad
18 changed files with 160 additions and 92 deletions

View File

@ -33,7 +33,7 @@ internal class SharedPreferencesDelegate(
override suspend fun clear() {
coroutineDispatchers.withIoContext {
preferences.edit().clear().apply()
preferences.edit().clear().commit()
}
}
}

View File

@ -1,26 +1,27 @@
package app.dapk.st
import android.app.Application
import android.content.Intent
import android.util.Log
import app.dapk.st.core.CoreAndroidModule
import app.dapk.st.core.ModuleProvider
import app.dapk.st.core.ProvidableModule
import app.dapk.st.core.attachAppLogger
import app.dapk.st.core.extensions.ResettableUnsafeLazy
import app.dapk.st.core.extensions.Scope
import app.dapk.st.core.extensions.unsafeLazy
import app.dapk.st.directory.DirectoryModule
import app.dapk.st.messenger.MessengerModule
import app.dapk.st.domain.StoreModule
import app.dapk.st.graph.AppModule
import app.dapk.st.graph.FeatureModules
import app.dapk.st.home.HomeModule
import app.dapk.st.login.LoginModule
import app.dapk.st.messenger.MessengerModule
import app.dapk.st.notifications.NotificationsModule
import app.dapk.st.notifications.PushAndroidService
import app.dapk.st.profile.ProfileModule
import app.dapk.st.settings.SettingsModule
import app.dapk.st.work.TaskRunnerModule
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.cancel
import kotlin.reflect.KClass
class SmallTalkApplication : Application(), ModuleProvider {
@ -28,8 +29,10 @@ class SmallTalkApplication : Application(), ModuleProvider {
private val appLogger: (String, String) -> Unit = { tag, message -> _appLogger?.invoke(tag, message) }
private var _appLogger: ((String, String) -> Unit)? = null
private val appModule: AppModule by unsafeLazy { AppModule(this, appLogger) }
private val featureModules: FeatureModules by unsafeLazy { appModule.featureModules }
private val lazyAppModule = ResettableUnsafeLazy { AppModule(this, appLogger) }
private val lazyFeatureModules = ResettableUnsafeLazy { appModule.featureModules }
private val appModule by lazyAppModule
private val featureModules by lazyFeatureModules
private val applicationScope = Scope(Dispatchers.IO)
override fun onCreate() {
@ -40,13 +43,15 @@ class SmallTalkApplication : Application(), ModuleProvider {
val logger: (String, String) -> Unit = { tag, message ->
Log.e(tag, message)
GlobalScope.launch {
eventLogStore.insert(tag, message)
}
applicationScope.launch { eventLogStore.insert(tag, message) }
}
attachAppLogger(logger)
_appLogger = logger
onApplicationLaunch(notificationsModule, storeModule)
}
private fun onApplicationLaunch(notificationsModule: NotificationsModule, storeModule: StoreModule) {
applicationScope.launch {
notificationsModule.firebasePushTokenUseCase().registerCurrentToken()
storeModule.localEchoStore.preload()
@ -73,4 +78,17 @@ class SmallTalkApplication : Application(), ModuleProvider {
else -> throw IllegalArgumentException("Unknown: $klass")
} as T
}
override fun reset() {
featureModules.notificationsModule.firebasePushTokenUseCase().unregister()
appModule.coroutineDispatchers.io.cancel()
applicationScope.cancel()
stopService(Intent(this, PushAndroidService::class.java))
lazyAppModule.reset()
lazyFeatureModules.reset()
val notificationsModule = featureModules.notificationsModule
val storeModule = appModule.storeModule.value
onApplicationLaunch(notificationsModule, storeModule)
}
}

View File

@ -0,0 +1,13 @@
package app.dapk.st.graph
import app.dapk.st.core.Base64
class AndroidBase64 : Base64 {
override fun encode(input: ByteArray): String {
return android.util.Base64.encodeToString(input, android.util.Base64.DEFAULT)
}
override fun decode(input: String): ByteArray {
return android.util.Base64.decode(input, android.util.Base64.DEFAULT)
}
}

View File

@ -69,7 +69,7 @@ internal class AppModule(context: Application, logger: MatrixLogger) {
private val driver = AndroidSqliteDriver(DapkDb.Schema, context, "dapk.db")
private val database = DapkDb(driver)
private val clock = Clock.systemUTC()
private val coroutineDispatchers = CoroutineDispatchers(Dispatchers.IO)
val coroutineDispatchers = CoroutineDispatchers(Dispatchers.IO)
val storeModule = unsafeLazy {
StoreModule(
@ -83,9 +83,12 @@ internal class AppModule(context: Application, logger: MatrixLogger) {
sql = "SELECT name FROM sqlite_master WHERE type = 'table' ${if (includeCryptoAccount) "" else "AND name != 'dbCryptoAccount'"}",
parameters = 0
)
while (cursor.next()) {
cursor.getString(0)?.let {
driver.execute(null, "DELETE FROM $it", 0)
cursor.use {
while (cursor.next()) {
cursor.getString(0)?.let {
log(AppLogTag.ERROR_NON_FATAL, "Deleting $it")
driver.execute(null, "DELETE FROM $it", 0)
}
}
}
},
@ -283,6 +286,14 @@ internal class MatrixModules(
store.roomStore(),
store.syncStore(),
store.filterStore(),
deviceNotifier = { services ->
val encryption = services.deviceService()
val crypto = services.cryptoService()
DeviceNotifier { userIds, syncToken ->
encryption.updateStaleDevices(userIds)
crypto.updateOlmSession(userIds, syncToken)
}
},
messageDecrypter = { serviceProvider ->
val cryptoService = serviceProvider.cryptoService()
MessageDecrypter {
@ -296,9 +307,9 @@ internal class MatrixModules(
}
},
verificationHandler = { services ->
logger.matrixLog(MatrixLogTag.VERIFICATION, "got a verification request $it")
val cryptoService = services.cryptoService()
VerificationHandler { apiEvent ->
logger.matrixLog(MatrixLogTag.VERIFICATION, "got a verification request $it")
cryptoService.onVerificationEvent(
when (apiEvent) {
is ApiToDeviceEvent.VerificationRequest -> Verification.Event.Requested(
@ -341,14 +352,6 @@ internal class MatrixModules(
)
}
},
deviceNotifier = { services ->
val encryption = services.deviceService()
val crypto = services.cryptoService()
DeviceNotifier { userIds, syncToken ->
encryption.updateStaleDevices(userIds)
crypto.updateOlmSession(userIds, syncToken)
}
},
oneTimeKeyProducer = { services ->
val cryptoService = services.cryptoService()
MaybeCreateMoreKeys {
@ -367,8 +370,6 @@ internal class MatrixModules(
)
installPushService(credentialsStore)
}
}
}
@ -414,13 +415,3 @@ class TaskRunnerAdapter(private val matrixTaskRunner: suspend (MatrixTask) -> Ma
}
}
}
class AndroidBase64 : Base64 {
override fun encode(input: ByteArray): String {
return android.util.Base64.encodeToString(input, android.util.Base64.DEFAULT)
}
override fun decode(input: String): ByteArray {
return android.util.Base64.decode(input, android.util.Base64.DEFAULT)
}
}

View File

@ -4,7 +4,8 @@ import kotlin.reflect.KClass
interface ModuleProvider {
fun <T: ProvidableModule> provide(klass: KClass<T>): T
fun <T : ProvidableModule> provide(klass: KClass<T>): T
fun reset()
}
interface ProvidableModule

View File

@ -21,3 +21,25 @@ inline fun <T, T1 : T, T2 : T> Iterable<T>.firstOrNull(predicate: (T) -> Boolean
}
fun <T> unsafeLazy(initializer: () -> T): Lazy<T> = lazy(mode = LazyThreadSafetyMode.NONE, initializer = initializer)
class ResettableUnsafeLazy<T>(private val initializer: () -> T) : Lazy<T> {
private var _value: T? = null
override val value: T
get() {
return if (_value == null) {
initializer().also { _value = it }
} else {
_value!!
}
}
override fun isInitialized(): Boolean {
return _value != null
}
fun reset() {
_value = null
}
}

View File

@ -4,3 +4,5 @@ import android.content.Context
inline fun <reified T : ProvidableModule> Context.module() =
(this.applicationContext as ModuleProvider).provide(T::class)
fun Context.resetModules() = (this.applicationContext as ModuleProvider).reset()

View File

@ -12,6 +12,10 @@ class RegisterFirebasePushTokenUseCase(
override val errorTracker: ErrorTracker,
) : CrashScope {
fun unregister() {
FirebaseMessaging.getInstance().deleteToken()
}
suspend fun registerCurrentToken() {
kotlin.runCatching {
FirebaseMessaging.getInstance().token().also {

View File

@ -1,5 +1,7 @@
package app.dapk.st.domain
import app.dapk.st.core.AppLogTag
import app.dapk.st.core.log
import app.dapk.st.matrix.common.SyncToken
import app.dapk.st.matrix.sync.SyncStore
import app.dapk.st.matrix.sync.SyncStore.SyncKey
@ -9,11 +11,13 @@ internal class SyncTokenPreferences(
) : SyncStore {
override suspend fun store(key: SyncKey, syncToken: SyncToken) {
log(AppLogTag.ERROR_NON_FATAL, "Store token :$syncToken")
preferences.store(key.value, syncToken.value)
}
override suspend fun read(key: SyncKey): SyncToken? {
return preferences.readString(key.value)?.let {
log(AppLogTag.ERROR_NON_FATAL, "Read token :$it")
SyncToken(it)
}
}

View File

@ -7,6 +7,7 @@ import androidx.compose.material.Surface
import androidx.compose.ui.Modifier
import app.dapk.st.core.DapkActivity
import app.dapk.st.core.module
import app.dapk.st.core.resetModules
import app.dapk.st.core.viewModel
import app.dapk.st.design.components.SmallTalkTheme
@ -20,6 +21,7 @@ class SettingsActivity : DapkActivity() {
SmallTalkTheme {
Surface(Modifier.fillMaxSize()) {
SettingsScreen(settingsViewModel, onSignOut = {
resetModules()
navigator.navigate.toHome()
finish()
}, navigator)

View File

@ -19,7 +19,6 @@ class SettingsModule(
) : ProvidableModule {
internal fun settingsViewModel() = SettingsViewModel(
storeModule.credentialsStore(),
storeModule.cacheCleaner(),
contentResolver,
cryptoService,

View File

@ -3,10 +3,11 @@ package app.dapk.st.settings
import android.content.ContentResolver
import android.net.Uri
import androidx.lifecycle.viewModelScope
import app.dapk.st.core.AppLogTag
import app.dapk.st.core.Lce
import app.dapk.st.core.log
import app.dapk.st.design.components.SpiderPage
import app.dapk.st.domain.StoreCleaner
import app.dapk.st.matrix.common.CredentialsStore
import app.dapk.st.matrix.crypto.CryptoService
import app.dapk.st.matrix.sync.SyncService
import app.dapk.st.settings.SettingItem.Id.*
@ -19,7 +20,6 @@ import kotlinx.coroutines.launch
private const val PRIVACY_POLICY_URL = "https://ouchadam.github.io/small-talk/privacy/"
internal class SettingsViewModel(
private val credentialsStore: CredentialsStore,
private val cacheCleaner: StoreCleaner,
private val contentResolver: ContentResolver,
private val cryptoService: CryptoService,
@ -49,9 +49,9 @@ internal class SettingsViewModel(
when (item.id) {
SignOut -> {
viewModelScope.launch {
credentialsStore.clear()
log(AppLogTag.ERROR_NON_FATAL, "Sign out triggered")
cacheCleaner.cleanCache(removeCredentials = true)
_events.emit(SignedOut)
println("emitted")
}
}
AccessToken -> {

View File

@ -38,7 +38,6 @@ internal class SettingsViewModelTest {
private val fakeSettingsItemFactory = FakeSettingsItemFactory()
private val viewModel = SettingsViewModel(
fakeCredentialsStore,
fakeStoreCleaner,
fakeContentResolver.instance,
fakeCryptoService,
@ -77,8 +76,8 @@ internal class SettingsViewModelTest {
}
@Test
fun `when sign out clicked, then clears credentials`() = runViewModelTest {
fakeCredentialsStore.expectUnit { it.clear() }
fun `when sign out clicked, then clears store`() = runViewModelTest {
fakeStoreCleaner.expectUnit { it.cleanCache(removeCredentials = true) }
val aSignOutItem = aSettingTextItem(id = SettingItem.Id.SignOut)
viewModel.test().onClick(aSignOutItem)

View File

@ -23,7 +23,6 @@ interface RoomStore {
interface FilterStore {
suspend fun store(key: String, filterId: String)
suspend fun read(key: String): String?
}

View File

@ -2,6 +2,7 @@ package app.dapk.st.matrix.sync.internal
import app.dapk.st.core.CoroutineDispatchers
import app.dapk.st.core.extensions.ErrorTracker
import app.dapk.st.core.withIoContext
import app.dapk.st.matrix.common.*
import app.dapk.st.matrix.http.MatrixHttpClient
import app.dapk.st.matrix.sync.*
@ -12,11 +13,15 @@ import app.dapk.st.matrix.sync.internal.room.RoomEventsDecrypter
import app.dapk.st.matrix.sync.internal.room.SyncEventDecrypter
import app.dapk.st.matrix.sync.internal.room.SyncSideEffects
import app.dapk.st.matrix.sync.internal.sync.*
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.*
import kotlinx.serialization.json.Json
import java.util.concurrent.atomic.AtomicInteger
private val syncSubscriptionCount = AtomicInteger()
internal class DefaultSyncService(
httpClient: MatrixHttpClient,
syncStore: SyncStore,
@ -34,11 +39,10 @@ internal class DefaultSyncService(
roomMembersService: RoomMembersService,
logger: MatrixLogger,
errorTracker: ErrorTracker,
coroutineDispatchers: CoroutineDispatchers,
private val coroutineDispatchers: CoroutineDispatchers,
syncConfig: SyncConfig,
) : SyncService {
private val syncSubscriptionCount = AtomicInteger()
private val syncEventsFlow = MutableStateFlow<List<SyncService.SyncEvent>>(emptyList())
private val roomDataSource by lazy { RoomDataSource(roomStore, logger) }
@ -107,7 +111,7 @@ internal class DefaultSyncService(
override fun events() = syncEventsFlow
override suspend fun observeEvent(eventId: EventId) = roomStore.observeEvent(eventId)
override suspend fun forceManualRefresh(roomIds: List<RoomId>) {
withContext(Dispatchers.IO) {
coroutineDispatchers.withIoContext {
roomIds.map {
async {
roomRefresher.refreshRoomContent(it)?.also {

View File

@ -34,18 +34,22 @@ internal class SyncReducer(
val newRooms = response.rooms?.join?.keys?.filterNot { roomDataSource.contains(it) } ?: emptyList()
val apiUpdatedRooms = response.rooms?.join?.keepRoomsWithChanges()
val apiRoomsToProcess = apiUpdatedRooms?.map { (roomId, apiRoom) ->
val apiRoomsToProcess = apiUpdatedRooms?.mapNotNull { (roomId, apiRoom) ->
logger.matrixLog(SYNC, "reducing: $roomId")
coroutineDispatchers.withIoContextAsync {
roomProcessor.processRoom(
roomToProcess = RoomToProcess(
roomId = roomId,
apiSyncRoom = apiRoom,
directMessage = directMessages[roomId],
userCredentials = userCredentials,
),
isInitialSync = isInitialSync
)
runCatching {
roomProcessor.processRoom(
roomToProcess = RoomToProcess(
roomId = roomId,
apiSyncRoom = apiRoom,
directMessage = directMessages[roomId],
userCredentials = userCredentials,
),
isInitialSync = isInitialSync
)
}
.onFailure { logger.matrixLog(SYNC, "failed to reduce: $roomId, skipping") }
.getOrNull()
}
} ?: emptyList()

View File

@ -8,9 +8,12 @@ import app.dapk.st.matrix.sync.internal.SideEffectFlowIterator
import app.dapk.st.matrix.sync.internal.overview.ReducedSyncFilterUseCase
import app.dapk.st.matrix.sync.internal.request.syncRequest
import app.dapk.st.matrix.sync.internal.room.SyncSideEffects
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.cancellable
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.isActive
import kotlinx.coroutines.isActive
internal class SyncUseCase(
private val persistence: OverviewStore,
@ -27,40 +30,43 @@ internal class SyncUseCase(
fun sync(): Flow<Unit> {
return flow<Unit> {
logger.matrixLog("flow instance: ${hashCode()}")
val credentials = credentialsStore.credentials()!!
val filterId = filterUseCase.reducedFilter(credentials.userId)
with(flowIterator) {
loop<OverviewState>(initial = null) { previousState ->
logger.matrixLog("looper : ${hashCode()}")
val syncToken = syncStore.read(key = SyncStore.SyncKey.Overview)
val response = doSyncRequest(filterId, syncToken)
logger.logP("sync processing") {
syncStore.store(key = SyncStore.SyncKey.Overview, syncToken = response.nextBatch)
val sideEffects = logger.logP("side effects processing") {
syncSideEffects.blockingSideEffects(credentials.userId, response, syncToken)
}
if (credentialsStore.isSignedIn()) {
logger.logP("sync processing") {
syncStore.store(key = SyncStore.SyncKey.Overview, syncToken = response.nextBatch)
val sideEffects = logger.logP("side effects processing") {
syncSideEffects.blockingSideEffects(credentials.userId, response, syncToken)
}
val isInitialSync = syncToken == null
val nextState = logger.logP("reducing") { syncReducer.reduce(isInitialSync, sideEffects, response, credentials) }
val overview = nextState.roomState.map { it.roomOverview }
val isInitialSync = syncToken == null
val nextState = logger.logP("reducing") { syncReducer.reduce(isInitialSync, sideEffects, response, credentials) }
val overview = nextState.roomState.map { it.roomOverview }
if (nextState.roomsLeft.isNotEmpty()) {
persistence.removeRooms(nextState.roomsLeft)
}
if (nextState.invites.isNotEmpty()) {
persistence.persistInvites(nextState.invites)
}
if (nextState.newRoomsJoined.isNotEmpty()) {
persistence.removeInvites(nextState.newRoomsJoined)
}
if (nextState.roomsLeft.isNotEmpty()) {
persistence.removeRooms(nextState.roomsLeft)
}
if (nextState.invites.isNotEmpty()) {
persistence.persistInvites(nextState.invites)
}
if (nextState.newRoomsJoined.isNotEmpty()) {
persistence.removeInvites(nextState.newRoomsJoined)
}
when {
previousState == overview -> previousState.also { logger.matrixLog(SYNC, "no changes, not persisting new state") }
overview.isNotEmpty() -> overview.also { persistence.persist(overview) }
else -> previousState.also { logger.matrixLog(SYNC, "nothing to do") }
when {
previousState == overview -> previousState.also { logger.matrixLog(SYNC, "no changes, not persisting new state") }
overview.isNotEmpty() -> overview.also { persistence.persist(overview) }
else -> previousState.also { logger.matrixLog(SYNC, "nothing to do") }
}
}
} else {
logger.matrixLog(SYNC, "sync processing skipped due to being signed out")
null
}
}
}

View File

@ -157,6 +157,14 @@ class TestMatrix(
storeModule.roomStore(),
storeModule.syncStore(),
storeModule.filterStore(),
deviceNotifier = { services ->
val encryptionService = services.deviceService()
val cryptoService = services.cryptoService()
DeviceNotifier { userIds, syncToken ->
encryptionService.updateStaleDevices(userIds)
cryptoService.updateOlmSession(userIds, syncToken)
}
},
messageDecrypter = { serviceProvider ->
MessageDecrypter {
serviceProvider.cryptoService().decrypt(it)
@ -222,14 +230,6 @@ class TestMatrix(
)
}
},
deviceNotifier = { services ->
val encryptionService = services.deviceService()
val cryptoService = services.cryptoService()
DeviceNotifier { userIds, syncToken ->
encryptionService.updateStaleDevices(userIds)
cryptoService.updateOlmSession(userIds, syncToken)
}
},
oneTimeKeyProducer = { services ->
val cryptoService = services.cryptoService()
MaybeCreateMoreKeys {