diff --git a/common/gecko/sample/src/main/java/com/dimension/maskbook/common/gecko/sample/MainActivity.kt b/common/gecko/sample/src/main/java/com/dimension/maskbook/common/gecko/sample/MainActivity.kt index 65223a7a..6383141d 100644 --- a/common/gecko/sample/src/main/java/com/dimension/maskbook/common/gecko/sample/MainActivity.kt +++ b/common/gecko/sample/src/main/java/com/dimension/maskbook/common/gecko/sample/MainActivity.kt @@ -43,16 +43,16 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.lifecycleScope import com.dimension.maskbook.common.gecko.WebContent import com.dimension.maskbook.common.gecko.WebContentController -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers class MainActivity : FragmentActivity() { lateinit var controller: WebContentController override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - controller = WebContentController(this, CoroutineScope(Dispatchers.IO)).apply { + controller = WebContentController(this, lifecycleScope, Dispatchers.IO).apply { installExtensions( "borderify@mozilla.org", "resource://android/assets/extensions/borderify/" diff --git a/common/gecko/src/androidMain/kotlin/com/dimension/maskbook/common/gecko/WebContentController.kt b/common/gecko/src/androidMain/kotlin/com/dimension/maskbook/common/gecko/WebContentController.kt index 7d1dc85e..e71ce3d6 100644 --- a/common/gecko/src/androidMain/kotlin/com/dimension/maskbook/common/gecko/WebContentController.kt +++ b/common/gecko/src/androidMain/kotlin/com/dimension/maskbook/common/gecko/WebContentController.kt @@ -25,15 +25,18 @@ import android.content.Intent import android.net.Uri import android.util.Log import androidx.fragment.app.FragmentActivity +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull @@ -64,7 +67,7 @@ private class MessageHolder : MessageHandler { private val _message = MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE) val message = _message.asSharedFlow() private val _port = MutableStateFlow(null) - val connected = _port.map { it != null } + val connected = _port.asStateFlow().map { it != null } override fun onPortConnected(port: Port) { _port.tryEmit(port) } @@ -91,6 +94,7 @@ private class MessageHolder : MessageHandler { class WebContentController( context: Context, private val scope: CoroutineScope, + private val dispatcher: CoroutineDispatcher, var onNavigate: (String) -> Boolean = { true }, ) : Closeable { private val _browserState = MutableStateFlow(null) @@ -108,7 +112,8 @@ class WebContentController( Log.i(TAG, "onBackgroundMessage: $it") it } - val isExtensionConnected = _backgroundMessageHolder.connected + val isExtensionConnected get() = _backgroundMessageHolder.connected + private val runtime by lazy { GeckoRuntime.create(context) } @@ -189,12 +194,12 @@ class WebContentController( } } } - }.launchIn(scope) + }.flowOn(dispatcher).launchIn(scope) _browserState.mapNotNull { it?.closedTabs }.onEach { list -> list.forEach { tab -> _contentMessageHolders.value -= tab.id } - }.launchIn(scope) + }.flowOn(dispatcher).launchIn(scope) } ) } @@ -233,7 +238,7 @@ class WebContentController( fun sendContentMessage(message: JSONObject) { Log.i(TAG, "sendContentMessage: $message") - scope.launch { + scope.launch(dispatcher) { _contentMessageHolders.firstOrNull()?.let { holders -> _activeTabId.firstOrNull()?.let { tabId -> holders[tabId]?.sendMessage(message) diff --git a/common/src/androidMain/kotlin/com/dimension/maskbook/common/CommonSetup.kt b/common/src/androidMain/kotlin/com/dimension/maskbook/common/CommonSetup.kt index 20c0ca25..65b1ba2a 100644 --- a/common/src/androidMain/kotlin/com/dimension/maskbook/common/CommonSetup.kt +++ b/common/src/androidMain/kotlin/com/dimension/maskbook/common/CommonSetup.kt @@ -22,18 +22,14 @@ package com.dimension.maskbook.common import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder +import com.dimension.maskbook.common.di.module.coroutinesModule import com.dimension.maskbook.common.manager.ImageLoaderManager import com.dimension.maskbook.common.manager.KeyStoreManager import com.dimension.maskbook.common.util.BiometricAuthenticator -import com.dimension.maskbook.common.util.coroutineExceptionHandler import com.dimension.maskbook.common.viewmodel.BiometricEnableViewModel import com.dimension.maskbook.common.viewmodel.BiometricViewModel import com.dimension.maskbook.common.viewmodel.SetUpPaymentPasswordViewModel -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob import org.koin.androidx.viewmodel.dsl.viewModel -import org.koin.core.qualifier.named import org.koin.dsl.module object CommonSetup : ModuleSetup { @@ -41,9 +37,8 @@ object CommonSetup : ModuleSetup { } override fun dependencyInject() = module { - single(named(IoScopeName)) { - CoroutineScope(SupervisorJob() + Dispatchers.IO + coroutineExceptionHandler) - } + coroutinesModule() + single { BiometricAuthenticator() } single { KeyStoreManager(get()) } single { ImageLoaderManager(get()) } diff --git a/common/src/androidMain/kotlin/com/dimension/maskbook/common/Consts.kt b/common/src/androidMain/kotlin/com/dimension/maskbook/common/Consts.kt index c6daf0b8..711fe9bb 100644 --- a/common/src/androidMain/kotlin/com/dimension/maskbook/common/Consts.kt +++ b/common/src/androidMain/kotlin/com/dimension/maskbook/common/Consts.kt @@ -20,6 +20,4 @@ */ package com.dimension.maskbook.common -const val IoScopeName = "IoScope" - const val LocalBackupAccount = "" diff --git a/common/src/androidMain/kotlin/com/dimension/maskbook/common/ModuleSetup.kt b/common/src/androidMain/kotlin/com/dimension/maskbook/common/ModuleSetup.kt index 68e07959..9587ca3b 100644 --- a/common/src/androidMain/kotlin/com/dimension/maskbook/common/ModuleSetup.kt +++ b/common/src/androidMain/kotlin/com/dimension/maskbook/common/ModuleSetup.kt @@ -22,12 +22,13 @@ package com.dimension.maskbook.common import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder +import org.koin.core.Koin import org.koin.core.module.Module interface ModuleSetup { fun NavGraphBuilder.route(navController: NavController) fun dependencyInject(): Module - fun onExtensionReady() {} + fun onExtensionReady(koin: Koin) = Unit } fun ModuleSetup.route(builder: NavGraphBuilder, navController: NavController) = diff --git a/common/src/androidMain/kotlin/com/dimension/maskbook/common/di/module/CoroutinesModule.kt b/common/src/androidMain/kotlin/com/dimension/maskbook/common/di/module/CoroutinesModule.kt new file mode 100644 index 00000000..1adaab4c --- /dev/null +++ b/common/src/androidMain/kotlin/com/dimension/maskbook/common/di/module/CoroutinesModule.kt @@ -0,0 +1,56 @@ +/* + * Mask-Android + * + * Copyright (C) 2022 DimensionDev and Contributors + * + * This file is part of Mask-Android. + * + * Mask-Android is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Mask-Android is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Mask-Android. If not, see . + */ +package com.dimension.maskbook.common.di.module + +import com.dimension.maskbook.common.di.scope.appScope +import com.dimension.maskbook.common.di.scope.defaultDispatcher +import com.dimension.maskbook.common.di.scope.ioDispatcher +import com.dimension.maskbook.common.di.scope.mainDispatcher +import com.dimension.maskbook.common.di.scope.mainImmediateDispatcher +import com.dimension.maskbook.common.di.scope.preferenceCoroutineContext +import com.dimension.maskbook.common.di.scope.repositoryCoroutineContext +import com.dimension.maskbook.common.util.coroutineExceptionHandler +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.SupervisorJob +import org.koin.core.module.Module + +internal fun Module.coroutinesModule() { + single(defaultDispatcher) { Dispatchers.Default } + single(ioDispatcher) { Dispatchers.IO } + single(mainDispatcher) { Dispatchers.Main } + single(mainImmediateDispatcher) { Dispatchers.Main.immediate } + + single(preferenceCoroutineContext) { + NonCancellable + Dispatchers.Default + } + single(repositoryCoroutineContext) { + coroutineExceptionHandler + Dispatchers.Default + } + + single(appScope) { + CoroutineScope( + coroutineExceptionHandler + SupervisorJob() + ) + } +} diff --git a/common/src/androidMain/kotlin/com/dimension/maskbook/common/di/scope/CoroutinesQualifiers.kt b/common/src/androidMain/kotlin/com/dimension/maskbook/common/di/scope/CoroutinesQualifiers.kt new file mode 100644 index 00000000..87e053fb --- /dev/null +++ b/common/src/androidMain/kotlin/com/dimension/maskbook/common/di/scope/CoroutinesQualifiers.kt @@ -0,0 +1,33 @@ +/* + * Mask-Android + * + * Copyright (C) 2022 DimensionDev and Contributors + * + * This file is part of Mask-Android. + * + * Mask-Android is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Mask-Android is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Mask-Android. If not, see . + */ +package com.dimension.maskbook.common.di.scope + +import org.koin.core.qualifier.named + +val defaultDispatcher = named("DefaultDispatcher") +val ioDispatcher = named("IoDispatcher") +val mainDispatcher = named("MainDispatcher") +val mainImmediateDispatcher = named("MainImmediateDispatcher") + +val preferenceCoroutineContext = named("PreferenceCoroutineContext") +val repositoryCoroutineContext = named("RepositoryCoroutineContext") + +val appScope = named("AppScope") diff --git a/common/src/androidMain/kotlin/com/dimension/maskbook/common/viewmodel/BiometricEnableViewModel.kt b/common/src/androidMain/kotlin/com/dimension/maskbook/common/viewmodel/BiometricEnableViewModel.kt index 287c8989..5b2eb694 100644 --- a/common/src/androidMain/kotlin/com/dimension/maskbook/common/viewmodel/BiometricEnableViewModel.kt +++ b/common/src/androidMain/kotlin/com/dimension/maskbook/common/viewmodel/BiometricEnableViewModel.kt @@ -22,8 +22,10 @@ package com.dimension.maskbook.common.viewmodel import android.content.Context import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.dimension.maskbook.common.util.BiometricAuthenticator import com.dimension.maskbook.setting.export.SettingServices +import kotlinx.coroutines.launch class BiometricEnableViewModel( private val biometricAuthenticator: BiometricAuthenticator, @@ -40,7 +42,7 @@ class BiometricEnableViewModel( context = context, onSuccess = { onEnable.invoke() - repository.setBiometricEnabled(true) + setBiometricEnabled(true) }, title = title, subTitle = subTitle, @@ -48,5 +50,11 @@ class BiometricEnableViewModel( ) } + fun setBiometricEnabled(enabled: Boolean) { + viewModelScope.launch { + repository.setBiometricEnabled(enabled) + } + } + fun isSupported(context: Context) = biometricAuthenticator.canAuthenticate(context = context) } diff --git a/common/src/androidMain/kotlin/com/dimension/maskbook/common/viewmodel/SetUpPaymentPasswordViewModel.kt b/common/src/androidMain/kotlin/com/dimension/maskbook/common/viewmodel/SetUpPaymentPasswordViewModel.kt index 879b6756..bda6f266 100644 --- a/common/src/androidMain/kotlin/com/dimension/maskbook/common/viewmodel/SetUpPaymentPasswordViewModel.kt +++ b/common/src/androidMain/kotlin/com/dimension/maskbook/common/viewmodel/SetUpPaymentPasswordViewModel.kt @@ -27,6 +27,7 @@ import com.dimension.maskbook.common.ext.asStateIn import com.dimension.maskbook.setting.export.SettingServices import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch class SetUpPaymentPasswordViewModel( private val repository: SettingServices, @@ -55,6 +56,8 @@ class SetUpPaymentPasswordViewModel( } fun confirm() { - repository.setPaymentPassword(newPassword.value) + viewModelScope.launch { + repository.setPaymentPassword(newPassword.value) + } } } diff --git a/entry/src/androidMain/kotlin/com/dimension/maskbook/entry/EntrySetup.kt b/entry/src/androidMain/kotlin/com/dimension/maskbook/entry/EntrySetup.kt index 751bdc53..e61ece26 100644 --- a/entry/src/androidMain/kotlin/com/dimension/maskbook/entry/EntrySetup.kt +++ b/entry/src/androidMain/kotlin/com/dimension/maskbook/entry/EntrySetup.kt @@ -24,18 +24,23 @@ import android.content.Context import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import com.dimension.maskbook.common.ModuleSetup +import com.dimension.maskbook.common.di.scope.appScope +import com.dimension.maskbook.common.di.scope.ioDispatcher +import com.dimension.maskbook.common.di.scope.preferenceCoroutineContext import com.dimension.maskbook.common.route.Navigator import com.dimension.maskbook.entry.data.JSMethod -import com.dimension.maskbook.entry.repository.EntryRepository +import com.dimension.maskbook.entry.repository.PreferenceRepository import com.dimension.maskbook.entry.repository.entryDataStore import com.dimension.maskbook.entry.ui.scene.generatedRoute +import com.dimension.maskbook.entry.viewModel.IntroViewModel +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.merge import kotlinx.coroutines.launch +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.core.Koin import org.koin.dsl.module -import org.koin.mp.KoinPlatformTools object EntrySetup : ModuleSetup { override fun NavGraphBuilder.route(navController: NavController) { @@ -43,25 +48,33 @@ object EntrySetup : ModuleSetup { } override fun dependencyInject() = module { - single { EntryRepository(get().entryDataStore) } - single { JSMethod(get()) } + single { + PreferenceRepository( + get(preferenceCoroutineContext), + get().entryDataStore + ) + } + single { + JSMethod(get()) + } + viewModel { IntroViewModel(get()) } } - override fun onExtensionReady() { - KoinPlatformTools.defaultContext().get().get().apply { - CoroutineScope(Dispatchers.IO).launch { - launch { - merge( - openCreateWalletView(), - openDashboardView(), - openAppsView(), - openSettingsView(), - ).filter { uri -> - uri.isNotEmpty() - }.collect { uri -> - Navigator.deeplink(uri) - } - } + override fun onExtensionReady(koin: Koin) { + val appScope = koin.get(appScope) + val ioDispatcher = koin.get(ioDispatcher) + + appScope.launch(ioDispatcher) { + val jsMethod = koin.get() + merge( + jsMethod.openCreateWalletView(), + jsMethod.openDashboardView(), + jsMethod.openAppsView(), + jsMethod.openSettingsView(), + ).filter { uri -> + uri.isNotEmpty() + }.collect { uri -> + Navigator.deeplink(uri) } } } diff --git a/entry/src/androidMain/kotlin/com/dimension/maskbook/entry/repository/EntryRepository.kt b/entry/src/androidMain/kotlin/com/dimension/maskbook/entry/repository/PreferenceRepository.kt similarity index 85% rename from entry/src/androidMain/kotlin/com/dimension/maskbook/entry/repository/EntryRepository.kt rename to entry/src/androidMain/kotlin/com/dimension/maskbook/entry/repository/PreferenceRepository.kt index 224d64a2..28399954 100644 --- a/entry/src/androidMain/kotlin/com/dimension/maskbook/entry/repository/EntryRepository.kt +++ b/entry/src/androidMain/kotlin/com/dimension/maskbook/entry/repository/PreferenceRepository.kt @@ -26,26 +26,25 @@ import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.preferencesDataStore -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlin.coroutines.CoroutineContext private val ShouldShowEntryKey = booleanPreferencesKey("ShouldShowEntry") val Context.entryDataStore: DataStore by preferencesDataStore(name = "entry") -class EntryRepository( +class PreferenceRepository( + private val preferenceCoroutineScope: CoroutineContext, private val dataStore: DataStore, ) { - private val scope = CoroutineScope(Dispatchers.IO) val shouldShowEntry: Flow get() = dataStore.data.map { it[ShouldShowEntryKey] ?: true } - fun setShouldShowEntry(shouldShowEntry: Boolean) { - scope.launch { + suspend fun setShouldShowEntry(shouldShowEntry: Boolean) { + withContext(preferenceCoroutineScope) { dataStore.edit { it[ShouldShowEntryKey] = shouldShowEntry } diff --git a/entry/src/androidMain/kotlin/com/dimension/maskbook/entry/ui/App.kt b/entry/src/androidMain/kotlin/com/dimension/maskbook/entry/ui/App.kt index 35f412df..ff578269 100644 --- a/entry/src/androidMain/kotlin/com/dimension/maskbook/entry/ui/App.kt +++ b/entry/src/androidMain/kotlin/com/dimension/maskbook/entry/ui/App.kt @@ -33,7 +33,7 @@ import com.dimension.maskbook.labs.LabsSetup import com.dimension.maskbook.persona.PersonaSetup import com.dimension.maskbook.setting.SettingSetup import com.dimension.maskbook.wallet.WalletSetup -import org.koin.mp.KoinPlatformTools +import org.koin.core.context.GlobalContext @OptIn(ExperimentalAnimationApi::class) @Composable @@ -52,12 +52,14 @@ fun App( } private suspend fun warmingUp() { - KoinPlatformTools.defaultContext().get().get().ensureExtensionActive() - CommonSetup.onExtensionReady() - WalletSetup.onExtensionReady() - SettingSetup.onExtensionReady() - LabsSetup.onExtensionReady() - PersonaSetup.onExtensionReady() - EntrySetup.onExtensionReady() - ExtensionSetup.onExtensionReady() + val koin = GlobalContext.get() + + koin.get().ensureExtensionActive() + CommonSetup.onExtensionReady(koin) + WalletSetup.onExtensionReady(koin) + SettingSetup.onExtensionReady(koin) + LabsSetup.onExtensionReady(koin) + PersonaSetup.onExtensionReady(koin) + EntrySetup.onExtensionReady(koin) + ExtensionSetup.onExtensionReady(koin) } diff --git a/entry/src/androidMain/kotlin/com/dimension/maskbook/entry/ui/Router.kt b/entry/src/androidMain/kotlin/com/dimension/maskbook/entry/ui/Router.kt index 59e09ffd..5b592a24 100644 --- a/entry/src/androidMain/kotlin/com/dimension/maskbook/entry/ui/Router.kt +++ b/entry/src/androidMain/kotlin/com/dimension/maskbook/entry/ui/Router.kt @@ -34,7 +34,7 @@ import com.dimension.maskbook.common.ui.widget.RouteHost import com.dimension.maskbook.common.ui.widget.rememberMaskBottomSheetNavigator import com.dimension.maskbook.entry.BuildConfig import com.dimension.maskbook.entry.EntrySetup -import com.dimension.maskbook.entry.repository.EntryRepository +import com.dimension.maskbook.entry.repository.PreferenceRepository import com.dimension.maskbook.entry.route.EntryRoute import com.dimension.maskbook.extension.ExtensionSetup import com.dimension.maskbook.labs.LabsSetup @@ -92,7 +92,7 @@ fun Router( } private suspend fun getInitialRoute(): String { - val repository = KoinPlatformTools.defaultContext().get().get() + val repository = KoinPlatformTools.defaultContext().get().get() val shouldShowEntry = repository.shouldShowEntry.firstOrNull() ?: true if (shouldShowEntry) { return EntryRoute.Intro diff --git a/entry/src/androidMain/kotlin/com/dimension/maskbook/entry/ui/scene/IntroScene.kt b/entry/src/androidMain/kotlin/com/dimension/maskbook/entry/ui/scene/IntroScene.kt index 04df9d94..14b7c82f 100644 --- a/entry/src/androidMain/kotlin/com/dimension/maskbook/entry/ui/scene/IntroScene.kt +++ b/entry/src/androidMain/kotlin/com/dimension/maskbook/entry/ui/scene/IntroScene.kt @@ -67,8 +67,8 @@ import com.dimension.maskbook.common.route.navigationComposeAnimComposable import com.dimension.maskbook.common.route.navigationComposeAnimComposablePackage import com.dimension.maskbook.common.routeProcessor.annotations.NavGraphDestination import com.dimension.maskbook.entry.R -import com.dimension.maskbook.entry.repository.EntryRepository import com.dimension.maskbook.entry.route.EntryRoute +import com.dimension.maskbook.entry.viewModel.IntroViewModel import com.dimension.maskbook.persona.route.PersonaRoute import com.google.accompanist.insets.navigationBarsPadding import com.google.accompanist.insets.statusBarsPadding @@ -76,7 +76,7 @@ import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.HorizontalPager import com.google.accompanist.pager.PagerState import com.google.accompanist.pager.rememberPagerState -import org.koin.androidx.compose.get +import org.koin.androidx.compose.getViewModel private data class IntroData( @DrawableRes val img: Int, @@ -93,7 +93,7 @@ private data class IntroData( fun IntroScene( navController: NavController, ) { - val repository = get() + val viewModel = getViewModel() val introList = remember { listOf( IntroData( @@ -136,7 +136,7 @@ fun IntroScene( item = introList[page], isEnd = isEnd, onStartClick = { - repository.setShouldShowEntry(false) + viewModel.setShouldShowEntry(false) navController.navigate(PersonaRoute.Register.Init) { popUpTo(EntryRoute.Intro) { inclusive = true @@ -147,7 +147,7 @@ fun IntroScene( if (!isEnd) { TextButton( onClick = { - repository.setShouldShowEntry(false) + viewModel.setShouldShowEntry(false) navController.navigate(PersonaRoute.Register.Init) { popUpTo(EntryRoute.Intro) { inclusive = true diff --git a/wallet/src/androidMain/kotlin/com/dimension/maskbook/wallet/viewmodel/WelcomeViewModel.kt b/entry/src/androidMain/kotlin/com/dimension/maskbook/entry/viewModel/IntroViewModel.kt similarity index 62% rename from wallet/src/androidMain/kotlin/com/dimension/maskbook/wallet/viewmodel/WelcomeViewModel.kt rename to entry/src/androidMain/kotlin/com/dimension/maskbook/entry/viewModel/IntroViewModel.kt index a8d4ba07..fd1a8941 100644 --- a/wallet/src/androidMain/kotlin/com/dimension/maskbook/wallet/viewmodel/WelcomeViewModel.kt +++ b/entry/src/androidMain/kotlin/com/dimension/maskbook/entry/viewModel/IntroViewModel.kt @@ -18,25 +18,20 @@ * You should have received a copy of the GNU Affero General Public License * along with Mask-Android. If not, see . */ -package com.dimension.maskbook.wallet.viewmodel +package com.dimension.maskbook.entry.viewModel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.dimension.maskbook.common.ext.asStateIn -import com.dimension.maskbook.persona.export.PersonaServices -import kotlinx.coroutines.flow.MutableStateFlow +import com.dimension.maskbook.entry.repository.PreferenceRepository +import kotlinx.coroutines.launch -class WelcomeViewModel( - private val personaServices: PersonaServices, +class IntroViewModel( + private val repository: PreferenceRepository, ) : ViewModel() { - private val _persona = MutableStateFlow("") - val persona = _persona.asStateIn(viewModelScope, "") - fun setPersona(text: String) { - _persona.value = text - } - - fun onConfirm() { - personaServices.updateCurrentPersona(_persona.value) + fun setShouldShowEntry(value: Boolean) { + viewModelScope.launch { + repository.setShouldShowEntry(value) + } } } diff --git a/extension/export/src/commonMain/kotlin/com/dimension/maskbook/extension/export/ExtensionServices.kt b/extension/export/src/commonMain/kotlin/com/dimension/maskbook/extension/export/ExtensionServices.kt index 4b17bca6..80b8cd38 100644 --- a/extension/export/src/commonMain/kotlin/com/dimension/maskbook/extension/export/ExtensionServices.kt +++ b/extension/export/src/commonMain/kotlin/com/dimension/maskbook/extension/export/ExtensionServices.kt @@ -26,7 +26,7 @@ import kotlinx.coroutines.flow.Flow interface ExtensionServices { val site: Flow - fun setSite(site: Site) + suspend fun setSite(site: Site) val isExtensionActive: Flow suspend fun ensureExtensionActive() suspend fun runBackgroundJSMethod(method: String, isWait: Boolean, vararg args: Pair): String? diff --git a/extension/src/androidMain/kotlin/com/dimension/maskbook/extension/ExtensionServicesImpl.kt b/extension/src/androidMain/kotlin/com/dimension/maskbook/extension/ExtensionServicesImpl.kt index bbb17572..87c5c69c 100644 --- a/extension/src/androidMain/kotlin/com/dimension/maskbook/extension/ExtensionServicesImpl.kt +++ b/extension/src/androidMain/kotlin/com/dimension/maskbook/extension/ExtensionServicesImpl.kt @@ -37,7 +37,7 @@ internal class ExtensionServicesImpl( override val site: Flow get() = repository.currentSite - override fun setSite(site: Site) { + override suspend fun setSite(site: Site) { repository.setCurrentSite(site) } diff --git a/extension/src/androidMain/kotlin/com/dimension/maskbook/extension/ExtensionSetup.kt b/extension/src/androidMain/kotlin/com/dimension/maskbook/extension/ExtensionSetup.kt index 2e6da07a..23d7829e 100644 --- a/extension/src/androidMain/kotlin/com/dimension/maskbook/extension/ExtensionSetup.kt +++ b/extension/src/androidMain/kotlin/com/dimension/maskbook/extension/ExtensionSetup.kt @@ -27,8 +27,10 @@ import androidx.navigation.NavType import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.navArgument import androidx.navigation.navDeepLink -import com.dimension.maskbook.common.IoScopeName import com.dimension.maskbook.common.ModuleSetup +import com.dimension.maskbook.common.di.scope.appScope +import com.dimension.maskbook.common.di.scope.ioDispatcher +import com.dimension.maskbook.common.di.scope.repositoryCoroutineContext import com.dimension.maskbook.common.ext.navigateToHome import com.dimension.maskbook.common.gecko.WebContentController import com.dimension.maskbook.common.route.Deeplinks @@ -40,9 +42,11 @@ import com.dimension.maskbook.extension.route.ExtensionRoute import com.dimension.maskbook.extension.ui.WebContentScene import com.dimension.maskbook.extension.utils.BackgroundMessageChannel import com.dimension.maskbook.extension.utils.ContentMessageChannel -import org.koin.core.qualifier.named +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.koin.core.Koin import org.koin.dsl.module -import org.koin.mp.KoinPlatformTools object ExtensionSetup : ModuleSetup { override fun NavGraphBuilder.route(navController: NavController) { @@ -69,15 +73,38 @@ object ExtensionSetup : ModuleSetup { } override fun dependencyInject() = module { - single { WebContentController(get(), get(named(IoScopeName))) } - single { ExtensionRepository(get()) } - single { ExtensionServicesImpl(get(), get(), get()) } - single { BackgroundMessageChannel(get(), get(named(IoScopeName))) } - single { ContentMessageChannel(get(), get(named(IoScopeName))) } + single { + WebContentController(get(), get(appScope), get(ioDispatcher)) + } + single { + ExtensionRepository( + get(repositoryCoroutineContext), + get(), + ) + } + single { + BackgroundMessageChannel(get()) + } + single { + ContentMessageChannel(get()) + } + single { + ExtensionServicesImpl(get(), get(), get()) + } } - override fun onExtensionReady() { - KoinPlatformTools.defaultContext().get().get().startMessageCollect() - KoinPlatformTools.defaultContext().get().get().startMessageCollect() + override fun onExtensionReady(koin: Koin) { + val appScope = koin.get(appScope) + val dispatcher = koin.get(ioDispatcher) + + appScope.launch(dispatcher) { + koin.get().startMessageCollect() + } + appScope.launch(dispatcher) { + koin.get().startMessageCollect() + } + appScope.launch(dispatcher) { + koin.get().startCollect() + } } } diff --git a/extension/src/androidMain/kotlin/com/dimension/maskbook/extension/repository/ExtensionRepository.kt b/extension/src/androidMain/kotlin/com/dimension/maskbook/extension/repository/ExtensionRepository.kt index 223540ff..f56c4416 100644 --- a/extension/src/androidMain/kotlin/com/dimension/maskbook/extension/repository/ExtensionRepository.kt +++ b/extension/src/androidMain/kotlin/com/dimension/maskbook/extension/repository/ExtensionRepository.kt @@ -24,35 +24,21 @@ import com.dimension.maskbook.common.gecko.WebContentController import com.dimension.maskbook.extension.export.model.Site import com.dimension.maskbook.extension.ext.site import com.dimension.maskbook.extension.ext.url -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.InternalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlin.coroutines.CoroutineContext -@OptIn(InternalCoroutinesApi::class) class ExtensionRepository( + private val repositoryCoroutineContext: CoroutineContext, private val controller: WebContentController, ) { - private val scope = CoroutineScope(Dispatchers.IO) private val _currentSite = MutableStateFlow(Site.Twitter) - val currentSite = _currentSite.asSharedFlow() - fun setCurrentSite(site: Site) { - _currentSite.value = site - scope.launch { - // workaround for this case:set current site to Twitter first, then set current site to facebook, - // then go back to twitter tab, currentSite's value is still facebook, if we set current - // site to facebook again, _currentSite won't update due to MutableStateFlow won't emit - // same value twice - if (controller.url.firstOrNull()?.site != _currentSite.value) { - controller.loadUrl(_currentSite.value.url) - } - } - } - val isExtensionConnected = controller.isExtensionConnected + val currentSite = _currentSite.asStateFlow() + + val isExtensionConnected get() = controller.isExtensionConnected + init { controller.installExtensions( id = "info@dimension.com", @@ -61,16 +47,23 @@ class ExtensionRepository( controller.onNavigate = { onNavigate(it) } - scope.launch { - launch { - _currentSite.collect { - controller.loadUrl(it.url) - } - } - launch { - isExtensionConnected.first { it } - controller.loadUrl(_currentSite.value.url) - } + controller.loadUrl(currentSite.value.url) + } + + suspend fun setCurrentSite(site: Site) = withContext(repositoryCoroutineContext) { + _currentSite.value = site + // workaround for this case:set current site to Twitter first, then set current site to facebook, + // then go back to twitter tab, currentSite's value is still facebook, if we set current + // site to facebook again, _currentSite won't update due to MutableStateFlow won't emit + // same value twice + if (controller.url.firstOrNull()?.site != _currentSite.value) { + controller.loadUrl(_currentSite.value.url) + } + } + + suspend fun startCollect() { + _currentSite.collect { + controller.loadUrl(it.url) } } diff --git a/extension/src/androidMain/kotlin/com/dimension/maskbook/extension/utils/MessageChannel.kt b/extension/src/androidMain/kotlin/com/dimension/maskbook/extension/utils/MessageChannel.kt index 624f2510..1e382c63 100644 --- a/extension/src/androidMain/kotlin/com/dimension/maskbook/extension/utils/MessageChannel.kt +++ b/extension/src/androidMain/kotlin/com/dimension/maskbook/extension/utils/MessageChannel.kt @@ -23,21 +23,17 @@ package com.dimension.maskbook.extension.utils import com.dimension.maskbook.common.gecko.WebContentController import com.dimension.maskbook.extension.export.model.ExtensionId import com.dimension.maskbook.extension.export.model.ExtensionMessage -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import org.json.JSONObject import java.util.UUID import java.util.concurrent.ConcurrentHashMap internal abstract class MessageChannel( private val flow: Flow, - private val scope: CoroutineScope, ) { private val queue = ConcurrentHashMap>() @@ -46,9 +42,8 @@ internal abstract class MessageChannel( protected abstract fun sendMessage(message: JSONObject) - fun startMessageCollect() { - flow.onEach { onMessage(it) } - .launchIn(scope) + suspend fun startMessageCollect() { + flow.collect { onMessage(it) } } fun sendResponseMessage(map: Map) { @@ -87,7 +82,7 @@ internal abstract class MessageChannel( } fun subscribeMessage(vararg method: String): Flow { - return _extensionMessage.filter { it.method in method } + return extensionMessage.filter { it.method in method } } private fun onMessage(jsonObject: JSONObject) { @@ -130,10 +125,8 @@ internal abstract class MessageChannel( internal class BackgroundMessageChannel( private val controller: WebContentController, - scope: CoroutineScope, ) : MessageChannel( flow = controller.backgroundMessage, - scope = scope, ) { override fun sendMessage(message: JSONObject) { controller.sendBackgroundMessage(message) @@ -142,10 +135,8 @@ internal class BackgroundMessageChannel( internal class ContentMessageChannel( private val controller: WebContentController, - scope: CoroutineScope, ) : MessageChannel( flow = controller.contentMessage, - scope = scope, ) { override fun sendMessage(message: JSONObject) { controller.sendContentMessage(message) diff --git a/labs/src/androidMain/kotlin/com/dimension/maskbook/labs/LabsSetup.kt b/labs/src/androidMain/kotlin/com/dimension/maskbook/labs/LabsSetup.kt index 7ba97e29..2f0563ee 100644 --- a/labs/src/androidMain/kotlin/com/dimension/maskbook/labs/LabsSetup.kt +++ b/labs/src/androidMain/kotlin/com/dimension/maskbook/labs/LabsSetup.kt @@ -23,8 +23,11 @@ package com.dimension.maskbook.labs import android.content.Context import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder -import com.dimension.maskbook.common.IoScopeName import com.dimension.maskbook.common.ModuleSetup +import com.dimension.maskbook.common.di.scope.appScope +import com.dimension.maskbook.common.di.scope.ioDispatcher +import com.dimension.maskbook.common.di.scope.preferenceCoroutineContext +import com.dimension.maskbook.common.di.scope.repositoryCoroutineContext import com.dimension.maskbook.common.ui.tab.TabScreen import com.dimension.maskbook.labs.data.JSMethod import com.dimension.maskbook.labs.data.RedPacketMethod @@ -38,11 +41,13 @@ import com.dimension.maskbook.labs.ui.tab.LabsTabScreen import com.dimension.maskbook.labs.viewmodel.LabsViewModel import com.dimension.maskbook.labs.viewmodel.LuckDropViewModel import com.dimension.maskbook.labs.viewmodel.PluginSettingsViewModel +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import org.koin.androidx.viewmodel.dsl.viewModel -import org.koin.core.qualifier.named +import org.koin.core.Koin import org.koin.dsl.bind import org.koin.dsl.module -import org.koin.mp.KoinPlatformTools object LabsSetup : ModuleSetup { @@ -51,12 +56,24 @@ object LabsSetup : ModuleSetup { } override fun dependencyInject() = module { - single { AppRepository(get()) } single { - PreferenceRepository(get().labsDataStore, get(named(IoScopeName))) + PreferenceRepository( + get().labsDataStore, + get(preferenceCoroutineContext), + ) + } + single { + AppRepository( + get(repositoryCoroutineContext), + get(), + ) + } + single { + JSMethod(get()) + } + single { + RedPacketMethod(get()) } - single { JSMethod(get()) } - single { RedPacketMethod(get(named(IoScopeName)), get()) } single { LabsTabScreen() } bind TabScreen::class @@ -65,8 +82,15 @@ object LabsSetup : ModuleSetup { viewModel { (dataRaw: String, requestRaw: String?) -> LuckDropViewModel(dataRaw, requestRaw, get(), get()) } } - override fun onExtensionReady() { - KoinPlatformTools.defaultContext().get().get().init() - KoinPlatformTools.defaultContext().get().get().startSubscribe() + override fun onExtensionReady(koin: Koin) { + val appScope = koin.get(appScope) + val dispatcher = koin.get(ioDispatcher) + + appScope.launch(dispatcher) { + koin.get().init() + } + appScope.launch(dispatcher) { + koin.get().startCollect() + } } } diff --git a/labs/src/androidMain/kotlin/com/dimension/maskbook/labs/data/RedPacketMethod.kt b/labs/src/androidMain/kotlin/com/dimension/maskbook/labs/data/RedPacketMethod.kt index 4a27449c..6fd4459f 100644 --- a/labs/src/androidMain/kotlin/com/dimension/maskbook/labs/data/RedPacketMethod.kt +++ b/labs/src/androidMain/kotlin/com/dimension/maskbook/labs/data/RedPacketMethod.kt @@ -28,35 +28,28 @@ import com.dimension.maskbook.extension.export.ExtensionServices import com.dimension.maskbook.labs.model.SendMethodRequest import com.dimension.maskbook.labs.model.options.RedPacketOptions import com.dimension.maskbook.labs.route.LabsRoute -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach class RedPacketMethod( - private val scope: CoroutineScope, private val services: ExtensionServices, ) { - - fun startSubscribe() { - services.subscribeCurrentContentJSEvent(notifyRedPacket, claimOrRefundRedPacket) - .onEach { message -> - when (message.method) { - notifyRedPacket -> { - message.responseSuccess(true) - } - claimOrRefundRedPacket -> { - val options = message.params?.decodeJson() ?: return@onEach - val requestRaw = SendMethodRequest( - id = message.id, - jsonrpc = message.jsonrpc, - method = message.method, - ).encodeJson() - Navigator.navigate(LabsRoute.RedPacket.LuckyDrop(options.encodeJson(), requestRaw)) - // response in LuckDropViewModel - } + suspend fun startCollect() { + services.subscribeCurrentContentJSEvent(notifyRedPacket, claimOrRefundRedPacket).collect { message -> + when (message.method) { + notifyRedPacket -> { + message.responseSuccess(true) + } + claimOrRefundRedPacket -> { + val options = message.params?.decodeJson() ?: return@collect + val requestRaw = SendMethodRequest( + id = message.id, + jsonrpc = message.jsonrpc, + method = message.method, + ).encodeJson() + Navigator.navigate(LabsRoute.RedPacket.LuckyDrop(options.encodeJson(), requestRaw)) + // response in LuckDropViewModel } } - .launchIn(scope) + } } companion object { diff --git a/labs/src/androidMain/kotlin/com/dimension/maskbook/labs/repository/AppRepository.kt b/labs/src/androidMain/kotlin/com/dimension/maskbook/labs/repository/AppRepository.kt index a1f1c34b..b30037f4 100644 --- a/labs/src/androidMain/kotlin/com/dimension/maskbook/labs/repository/AppRepository.kt +++ b/labs/src/androidMain/kotlin/com/dimension/maskbook/labs/repository/AppRepository.kt @@ -23,16 +23,15 @@ package com.dimension.maskbook.labs.repository import com.dimension.maskbook.labs.data.JSMethod import com.dimension.maskbook.labs.export.model.AppData import com.dimension.maskbook.labs.export.model.AppKey -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlin.coroutines.CoroutineContext internal class AppRepository( + private val repositoryCoroutineContext: CoroutineContext, private val jsMethod: JSMethod, ) : IAppRepository { - private val scope = CoroutineScope(Dispatchers.IO) private val _apps = MutableStateFlow( AppKey.values().map { AppData(it, true) } @@ -48,16 +47,12 @@ internal class AppRepository( } } - override fun setEnabled(appKey: AppKey, enabled: Boolean) { - scope.launch { - jsMethod.setPluginStatus(appKey.id, enabled) - refreshApps() - } + override suspend fun setEnabled(appKey: AppKey, enabled: Boolean) = withContext(repositoryCoroutineContext) { + jsMethod.setPluginStatus(appKey.id, enabled) + refreshApps() } - override fun init() { - scope.launch { - refreshApps() - } + override suspend fun init() { + refreshApps() } } diff --git a/labs/src/androidMain/kotlin/com/dimension/maskbook/labs/repository/IAppRepository.kt b/labs/src/androidMain/kotlin/com/dimension/maskbook/labs/repository/IAppRepository.kt index cd2ac01d..0d32366c 100644 --- a/labs/src/androidMain/kotlin/com/dimension/maskbook/labs/repository/IAppRepository.kt +++ b/labs/src/androidMain/kotlin/com/dimension/maskbook/labs/repository/IAppRepository.kt @@ -26,6 +26,6 @@ import kotlinx.coroutines.flow.Flow interface IAppRepository { val apps: Flow> - fun setEnabled(appKey: AppKey, enabled: Boolean) - fun init() + suspend fun setEnabled(appKey: AppKey, enabled: Boolean) + suspend fun init() } diff --git a/labs/src/androidMain/kotlin/com/dimension/maskbook/labs/repository/IPreferenceRepository.kt b/labs/src/androidMain/kotlin/com/dimension/maskbook/labs/repository/IPreferenceRepository.kt index ae368709..3adb6ec3 100644 --- a/labs/src/androidMain/kotlin/com/dimension/maskbook/labs/repository/IPreferenceRepository.kt +++ b/labs/src/androidMain/kotlin/com/dimension/maskbook/labs/repository/IPreferenceRepository.kt @@ -24,5 +24,5 @@ import kotlinx.coroutines.flow.Flow interface IPreferenceRepository { val shouldShowPluginSettingsTipDialog: Flow - fun setShowPluginSettingsTipDialog(bool: Boolean) + suspend fun setShowPluginSettingsTipDialog(bool: Boolean) } diff --git a/labs/src/androidMain/kotlin/com/dimension/maskbook/labs/repository/PreferenceRepository.kt b/labs/src/androidMain/kotlin/com/dimension/maskbook/labs/repository/PreferenceRepository.kt index 44186833..f17a6081 100644 --- a/labs/src/androidMain/kotlin/com/dimension/maskbook/labs/repository/PreferenceRepository.kt +++ b/labs/src/androidMain/kotlin/com/dimension/maskbook/labs/repository/PreferenceRepository.kt @@ -26,17 +26,17 @@ import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.preferencesDataStore -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlin.coroutines.CoroutineContext private val ShouldShowPluginSettingsTipDialog = booleanPreferencesKey("ShouldShowPluginSettingsTipDialog") val Context.labsDataStore: DataStore by preferencesDataStore(name = "labs") class PreferenceRepository( private val dataStore: DataStore, - private val ioScope: CoroutineScope, + private val preferenceCoroutineContext: CoroutineContext, ) : IPreferenceRepository { override val shouldShowPluginSettingsTipDialog: Flow @@ -44,8 +44,8 @@ class PreferenceRepository( it[ShouldShowPluginSettingsTipDialog] ?: true } - override fun setShowPluginSettingsTipDialog(bool: Boolean) { - ioScope.launch { + override suspend fun setShowPluginSettingsTipDialog(bool: Boolean) { + withContext(preferenceCoroutineContext) { dataStore.edit { it[ShouldShowPluginSettingsTipDialog] = bool } diff --git a/labs/src/androidMain/kotlin/com/dimension/maskbook/labs/viewmodel/LabsViewModel.kt b/labs/src/androidMain/kotlin/com/dimension/maskbook/labs/viewmodel/LabsViewModel.kt index 4f81641a..420e56b6 100644 --- a/labs/src/androidMain/kotlin/com/dimension/maskbook/labs/viewmodel/LabsViewModel.kt +++ b/labs/src/androidMain/kotlin/com/dimension/maskbook/labs/viewmodel/LabsViewModel.kt @@ -32,6 +32,7 @@ import com.dimension.maskbook.wallet.export.WalletServices import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch data class AppDisplayData( val key: AppKey, @@ -82,7 +83,9 @@ class LabsViewModel( ) : ViewModel() { init { - repository.init() + viewModelScope.launch { + repository.init() + } } val apps by lazy { @@ -101,8 +104,4 @@ class LabsViewModel( val wallet by lazy { walletRepository.currentWallet.asStateIn(viewModelScope, null) } - - fun setEnabled(key: AppKey, enabled: Boolean) { - repository.setEnabled(key, enabled) - } } diff --git a/labs/src/androidMain/kotlin/com/dimension/maskbook/labs/viewmodel/PluginSettingsViewModel.kt b/labs/src/androidMain/kotlin/com/dimension/maskbook/labs/viewmodel/PluginSettingsViewModel.kt index d80c6747..41050ba5 100644 --- a/labs/src/androidMain/kotlin/com/dimension/maskbook/labs/viewmodel/PluginSettingsViewModel.kt +++ b/labs/src/androidMain/kotlin/com/dimension/maskbook/labs/viewmodel/PluginSettingsViewModel.kt @@ -30,9 +30,8 @@ import com.dimension.maskbook.labs.export.model.AppKey import com.dimension.maskbook.labs.repository.IAppRepository import com.dimension.maskbook.labs.repository.IPreferenceRepository import com.dimension.maskbook.wallet.export.WalletServices -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch data class PluginDisplayData( val key: AppKey, @@ -109,7 +108,6 @@ class PluginSettingsViewModel( ) } } - .flowOn(Dispatchers.IO) .asStateIn(viewModelScope, emptyList()) } @@ -118,7 +116,9 @@ class PluginSettingsViewModel( } fun setEnabled(key: AppKey, enabled: Boolean) { - repository.setEnabled(key, enabled) + viewModelScope.launch { + repository.setEnabled(key, enabled) + } } val shouldShowPluginSettingsTipDialog by lazy { @@ -127,6 +127,8 @@ class PluginSettingsViewModel( } fun setShowPluginSettingsTipDialog(bool: Boolean) { - preferenceRepository.setShowPluginSettingsTipDialog(bool) + viewModelScope.launch { + preferenceRepository.setShowPluginSettingsTipDialog(bool) + } } } diff --git a/persona/export/src/commonMain/kotlin/com/dimension/maskbook/persona/export/PersonaServices.kt b/persona/export/src/commonMain/kotlin/com/dimension/maskbook/persona/export/PersonaServices.kt index 150fe07c..7a7a1993 100644 --- a/persona/export/src/commonMain/kotlin/com/dimension/maskbook/persona/export/PersonaServices.kt +++ b/persona/export/src/commonMain/kotlin/com/dimension/maskbook/persona/export/PersonaServices.kt @@ -30,10 +30,10 @@ import kotlinx.coroutines.flow.Flow interface PersonaServices { val currentPersona: Flow suspend fun hasPersona(): Boolean - fun updateCurrentPersona(value: String) + suspend fun updateCurrentPersona(value: String) suspend fun createPersonaFromMnemonic(value: List, name: String) suspend fun createPersonaFromPrivateKey(value: String, name: String) - fun connectProfile(personaId: String, profileId: String) + suspend fun connectProfile(personaId: String, profileId: String) suspend fun createPersonaBackup(hasPrivateKeyOnly: Boolean): List suspend fun restorePersonaBackup(persona: List) suspend fun createProfileBackup(): List diff --git a/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/PersonaServicesImpl.kt b/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/PersonaServicesImpl.kt index 790db5af..610997ff 100644 --- a/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/PersonaServicesImpl.kt +++ b/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/PersonaServicesImpl.kt @@ -40,7 +40,7 @@ class PersonaServicesImpl( return personaRepository.hasPersona() } - override fun updateCurrentPersona(value: String) { + override suspend fun updateCurrentPersona(value: String) { personaRepository.updateCurrentPersona(value) } @@ -52,7 +52,7 @@ class PersonaServicesImpl( personaRepository.createPersonaFromPrivateKey(value, name) } - override fun connectProfile(personaId: String, profileId: String) { + override suspend fun connectProfile(personaId: String, profileId: String) { personaRepository.connectProfile(personaId, profileId) } diff --git a/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/PersonaSetup.kt b/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/PersonaSetup.kt index 67a9c4dd..150308ff 100644 --- a/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/PersonaSetup.kt +++ b/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/PersonaSetup.kt @@ -25,9 +25,13 @@ import androidx.compose.animation.ExperimentalAnimationApi import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.room.Room -import com.dimension.maskbook.common.IoScopeName import com.dimension.maskbook.common.LocalBackupAccount import com.dimension.maskbook.common.ModuleSetup +import com.dimension.maskbook.common.di.scope.appScope +import com.dimension.maskbook.common.di.scope.ioDispatcher +import com.dimension.maskbook.common.di.scope.mainDispatcher +import com.dimension.maskbook.common.di.scope.preferenceCoroutineContext +import com.dimension.maskbook.common.di.scope.repositoryCoroutineContext import com.dimension.maskbook.common.ui.tab.TabScreen import com.dimension.maskbook.persona.data.JSMethod import com.dimension.maskbook.persona.data.JSMethodV2 @@ -78,14 +82,15 @@ import com.dimension.maskbook.persona.viewmodel.register.RemoteBackupRecoveryVie import com.dimension.maskbook.persona.viewmodel.social.DisconnectSocialViewModel import com.dimension.maskbook.persona.viewmodel.social.UserNameModalViewModel import com.google.accompanist.navigation.animation.navigation -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.asExecutor +import kotlinx.coroutines.launch import org.koin.androidx.viewmodel.dsl.viewModel -import org.koin.core.qualifier.named +import org.koin.core.Koin import org.koin.dsl.bind import org.koin.dsl.binds import org.koin.dsl.module -import org.koin.mp.KoinPlatformTools object PersonaSetup : ModuleSetup { @@ -108,9 +113,10 @@ object PersonaSetup : ModuleSetup { override fun dependencyInject() = module { single { + val executor = get(ioDispatcher).asExecutor() Room.databaseBuilder(get(), PersonaDatabase::class.java, "maskbook_persona") - .setQueryExecutor(Dispatchers.IO.asExecutor()) - .setTransactionExecutor(Dispatchers.IO.asExecutor()) + .setQueryExecutor(executor) + .setTransactionExecutor(executor) .fallbackToDestructiveMigration() .addTypeConverter(EncryptStringConverter(get())) .addTypeConverter(EncryptJsonObjectConverter(get())) @@ -118,7 +124,9 @@ object PersonaSetup : ModuleSetup { } single { PersonaRepository( - get(named(IoScopeName)), + get(appScope), + get(repositoryCoroutineContext), + get(mainDispatcher), get(), get(), get(), get(), get(), get(), get(), @@ -131,14 +139,13 @@ object PersonaSetup : ModuleSetup { single { PreferenceRepository( get().personaDataStore, - get(named(IoScopeName)) + get(preferenceCoroutineContext), ) } single { JSMethod(get()) } single { JSMethodV2( - get(named(IoScopeName)), get(), get(), get(), get(), @@ -207,8 +214,18 @@ object PersonaSetup : ModuleSetup { viewModel { PersonaLogoutViewModel(get(), get()) } } - override fun onExtensionReady() { - KoinPlatformTools.defaultContext().get().get().init() - KoinPlatformTools.defaultContext().get().get().startSubscribe() + override fun onExtensionReady(koin: Koin) { + val appScope = koin.get(appScope) + val dispatcher = koin.get(ioDispatcher) + + appScope.launch(dispatcher) { + koin.get().init() + } + appScope.launch(dispatcher) { + koin.get().startCollect() + } + appScope.launch(dispatcher) { + koin.get().tryMigrateIndexedDb() + } } } diff --git a/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/data/JSMethodV2.kt b/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/data/JSMethodV2.kt index 733181e3..23cf3b25 100644 --- a/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/data/JSMethodV2.kt +++ b/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/data/JSMethodV2.kt @@ -59,14 +59,9 @@ import com.dimension.maskbook.persona.model.options.UpdateProfileOptions import com.dimension.maskbook.persona.model.options.UpdateRelationOptions import com.dimension.maskbook.persona.repository.IPersonaRepository import com.dimension.maskbook.persona.repository.IPreferenceRepository -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch class JSMethodV2( - private val scope: CoroutineScope, private val services: ExtensionServices, private val database: PersonaDatabase, private val personaRepository: IPersonaRepository, @@ -76,29 +71,28 @@ class JSMethodV2( private val relationDataSource: JsRelationDataSource, private val postDataSource: JsPostDataSource, ) { - fun startSubscribe() { - scope.launch { - if (preferenceRepository.isMigratorIndexedDb.first()) { - return@launch - } - val records: IndexedDBAllRecord? = services.execute("get_all_indexedDB_records") - if (records != null) { - IndexedDBDataMigrator.migrate(database, records) - preferenceRepository.setIsMigratorIndexedDb(true) - } + suspend fun tryMigrateIndexedDb() { + if (preferenceRepository.isMigratorIndexedDb.first()) { + return } - services.subscribeBackgroundJSEvent(*methods) - .onEach { - subscribeWithPersona(it) || - subscribeWithProfile(it) || - subscribeWithRelation(it) || - subscribeWithAvatar(it) || - subscribeWithPost(it) || - subscribeWithHelper(it) - } - .launchIn(scope) + val records: IndexedDBAllRecord? = services.execute("get_all_indexedDB_records") + if (records != null) { + IndexedDBDataMigrator.migrate(database, records) + preferenceRepository.setIsMigratorIndexedDb(true) + } + } + + suspend fun startCollect() { + services.subscribeBackgroundJSEvent(*methods).collect { + subscribeWithPersona(it) || + subscribeWithProfile(it) || + subscribeWithRelation(it) || + subscribeWithAvatar(it) || + subscribeWithPost(it) || + subscribeWithHelper(it) + } } // Persona @@ -122,7 +116,8 @@ class JSMethodV2( return message.responseSuccess(personaDataSource.queryPersona(options)) } queryPersonaByProfile -> { - val options = message.decodeOptions>()?.options ?: return true + val options = + message.decodeOptions>()?.options ?: return true return message.responseSuccess(personaDataSource.queryPersonaByProfile(options)) } queryPersonas -> { diff --git a/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/repository/IPersonaRepository.kt b/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/repository/IPersonaRepository.kt index bf7b7c3a..cb63e06e 100644 --- a/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/repository/IPersonaRepository.kt +++ b/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/repository/IPersonaRepository.kt @@ -41,16 +41,16 @@ interface IPersonaRepository { ) suspend fun logout() suspend fun setCurrentPersona(id: String) - fun updatePersona(id: String, nickname: String) - fun updateCurrentPersona(nickname: String) - fun connectProfile(personaId: String, profileId: String) - fun disconnectProfile(personaId: String, profileId: String) + suspend fun updatePersona(id: String, nickname: String) + suspend fun updateCurrentPersona(nickname: String) + suspend fun connectProfile(personaId: String, profileId: String) + suspend fun disconnectProfile(personaId: String, profileId: String) suspend fun createPersonaFromMnemonic(value: List, name: String) suspend fun createPersonaFromPrivateKey(value: String, name: String) suspend fun backupPrivateKey(id: String): String - fun init() - fun setPlatform(platformType: PlatformType) - fun setAvatarForCurrentPersona(avatar: Uri?) + suspend fun init() + suspend fun setPlatform(platformType: PlatformType) + suspend fun setAvatarForCurrentPersona(avatar: Uri?) suspend fun createPersonaBackup(hasPrivateKeyOnly: Boolean): List suspend fun restorePersonaBackup(list: List) suspend fun createProfileBackup(): List diff --git a/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/repository/IPreferenceRepository.kt b/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/repository/IPreferenceRepository.kt index b379a6d6..fa6f1d84 100644 --- a/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/repository/IPreferenceRepository.kt +++ b/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/repository/IPreferenceRepository.kt @@ -28,10 +28,10 @@ interface IPreferenceRepository { val currentPersonaIdentifier: Flow suspend fun setCurrentPersonaIdentifier(identifier: String) val shouldShowContactsTipDialog: Flow - fun setShowContactsTipDialog(bool: Boolean) + suspend fun setShowContactsTipDialog(bool: Boolean) val isMigratorIndexedDb: Flow - fun setIsMigratorIndexedDb(bool: Boolean) + suspend fun setIsMigratorIndexedDb(bool: Boolean) val lastDetectProfileIdentifier: Flow fun setLastDetectProfileIdentifier(identifier: String) diff --git a/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/repository/PersonaRepository.kt b/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/repository/PersonaRepository.kt index afd0c98e..d7924fe7 100644 --- a/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/repository/PersonaRepository.kt +++ b/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/repository/PersonaRepository.kt @@ -38,8 +38,8 @@ import com.dimension.maskbook.persona.export.model.PersonaData import com.dimension.maskbook.persona.export.model.PlatformType import com.dimension.maskbook.persona.export.model.SocialData import com.dimension.maskbook.persona.model.ContactData +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow @@ -52,9 +52,12 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlin.coroutines.CoroutineContext internal class PersonaRepository( - private val scope: CoroutineScope, + private val appScope: CoroutineScope, + private val repositoryCoroutineContext: CoroutineContext, + private val mainDispatcher: CoroutineDispatcher, private val jsMethod: JSMethod, private val extensionServices: ExtensionServices, private val preferenceRepository: IPreferenceRepository, @@ -100,40 +103,40 @@ internal class PersonaRepository( onDone: (ConnectAccountData) -> Unit, ) { connectingJob?.cancel() - extensionServices.setSite(platformType.toSite()) + appScope.launch { + extensionServices.setSite(platformType.toSite()) + } connectingJob = preferenceRepository.lastDetectProfileIdentifier .filterNot { it.isEmpty() } .filterNot { personaDataSource.hasConnected(it) } .flatMapLatest { profileDataSource.getSocialFlow(it) } .filterNotNull() - .flowOn(Dispatchers.IO) + .flowOn(repositoryCoroutineContext) .onEach { onDone.invoke(ConnectAccountData(personaId, it)) connectingJob?.cancel() } - .flowOn(Dispatchers.Main) - .launchIn(scope) + .flowOn(mainDispatcher) + .launchIn(appScope) } - override fun init() { - scope.launch { - if (personaDataSource.isEmpty()) { - return@launch - } - - val identifier = preferenceRepository.currentPersonaIdentifier.firstOrNull() - if (!identifier.isNullOrEmpty() && personaDataSource.contains(identifier)) { - return@launch - } + override suspend fun init() { + if (personaDataSource.isEmpty()) { + return + } - val newCurrentPersona = personaDataSource.getPersonaFirst() - setCurrentPersona(newCurrentPersona?.identifier.orEmpty()) + val identifier = preferenceRepository.currentPersonaIdentifier.firstOrNull() + if (!identifier.isNullOrEmpty() && personaDataSource.contains(identifier)) { + return } + + val newCurrentPersona = personaDataSource.getPersonaFirst() + setCurrentPersona(newCurrentPersona?.identifier.orEmpty()) } override suspend fun setCurrentPersona(id: String) { - withContext(scope.coroutineContext) { + withContext(repositoryCoroutineContext) { if (id.isEmpty() || personaDataSource.getPersona(id) != null) { preferenceRepository.setCurrentPersonaIdentifier(id) jsMethod.setCurrentPersonaIdentifier(id) @@ -142,7 +145,7 @@ internal class PersonaRepository( } override suspend fun logout() { - withContext(scope.coroutineContext) { + withContext(repositoryCoroutineContext) { val deletePersona = currentPersona.firstOrNull() ?: return@withContext // set current persona first ,avoid currentPersona emmit null if there has other personas val newCurrentPersona = personaDataSource.getPersonaList().firstOrNull { @@ -155,35 +158,35 @@ internal class PersonaRepository( } } - override fun updatePersona(id: String, nickname: String) { - scope.launch { + override suspend fun updatePersona(id: String, nickname: String) { + withContext(repositoryCoroutineContext) { personaDataSource.updateNickName(id, nickname) jsMethod.updatePersonaInfo(id, nickname) } } - override fun updateCurrentPersona(nickname: String) { - scope.launch { - val id = currentPersona.firstOrNull()?.identifier ?: return@launch + override suspend fun updateCurrentPersona(nickname: String) { + withContext(repositoryCoroutineContext) { + val id = currentPersona.firstOrNull()?.identifier ?: return@withContext personaDataSource.updateNickName(id, nickname) jsMethod.updatePersonaInfo(id, nickname) } } - override fun connectProfile(personaId: String, profileId: String) { - scope.launch { + override suspend fun connectProfile(personaId: String, profileId: String) { + withContext(repositoryCoroutineContext) { jsMethod.connectProfile(personaId, profileId) } } - override fun disconnectProfile(personaId: String, profileId: String) { - scope.launch { + override suspend fun disconnectProfile(personaId: String, profileId: String) { + withContext(repositoryCoroutineContext) { jsMethod.disconnectProfile(profileId) } } override suspend fun createPersonaFromMnemonic(value: List, name: String) { - withContext(scope.coroutineContext) { + withContext(repositoryCoroutineContext) { val mnemonic = value.joinToString(" ") if (personaDataSource.containsMnemonic(mnemonic)) { throw PersonaAlreadyExitsError() @@ -193,7 +196,7 @@ internal class PersonaRepository( } override suspend fun createPersonaFromPrivateKey(value: String, name: String) { - withContext(scope.coroutineContext) { + withContext(repositoryCoroutineContext) { if (personaDataSource.containsPrivateKey(value)) throw PersonaAlreadyExitsError() jsMethod.restoreFromPrivateKey(privateKey = value, nickname = name) } @@ -203,12 +206,14 @@ internal class PersonaRepository( return jsMethod.backupPrivateKey(id) ?: "" } - override fun setPlatform(platformType: PlatformType) { - extensionServices.setSite(platformType.toSite()) + override suspend fun setPlatform(platformType: PlatformType) { + withContext(repositoryCoroutineContext) { + extensionServices.setSite(platformType.toSite()) + } } - override fun setAvatarForCurrentPersona(avatar: Uri?) { - scope.launch { + override suspend fun setAvatarForCurrentPersona(avatar: Uri?) { + withContext(repositoryCoroutineContext) { currentPersona.firstOrNull()?.let { personaData -> personaDataSource.updateAvatar(personaData.identifier, avatar?.toString()) } diff --git a/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/repository/PreferenceRepository.kt b/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/repository/PreferenceRepository.kt index 69b49e47..ee33e36f 100644 --- a/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/repository/PreferenceRepository.kt +++ b/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/repository/PreferenceRepository.kt @@ -27,13 +27,12 @@ import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlin.coroutines.CoroutineContext private val CurrentPersonaKey = stringPreferencesKey("current_persona") private val ShouldShowContactsTipDialog = booleanPreferencesKey("ShouldShowContactsTipDialog") @@ -42,7 +41,7 @@ val Context.personaDataStore: DataStore by preferencesDataStore(nam class PreferenceRepository( private val dataStore: DataStore, - private val ioScope: CoroutineScope, + private val preferenceCoroutineContext: CoroutineContext, ) : IPreferenceRepository { override val data: Flow @@ -54,10 +53,8 @@ class PreferenceRepository( } override suspend fun setCurrentPersonaIdentifier(identifier: String) { - withContext(ioScope.coroutineContext) { - dataStore.edit { - it[CurrentPersonaKey] = identifier - } + dataStore.edit { + it[CurrentPersonaKey] = identifier } } @@ -66,8 +63,8 @@ class PreferenceRepository( it[ShouldShowContactsTipDialog] ?: true } - override fun setShowContactsTipDialog(bool: Boolean) { - ioScope.launch { + override suspend fun setShowContactsTipDialog(bool: Boolean) { + withContext(preferenceCoroutineContext) { dataStore.edit { it[ShouldShowContactsTipDialog] = bool } @@ -79,8 +76,8 @@ class PreferenceRepository( it[IsMigratorIndexedDb] ?: false } - override fun setIsMigratorIndexedDb(bool: Boolean) { - ioScope.launch { + override suspend fun setIsMigratorIndexedDb(bool: Boolean) { + withContext(preferenceCoroutineContext) { dataStore.edit { it[IsMigratorIndexedDb] = bool } diff --git a/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/ui/scenes/PersonaInfoScene.kt b/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/ui/scenes/PersonaInfoScene.kt index 80a94d52..22faf998 100644 --- a/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/ui/scenes/PersonaInfoScene.kt +++ b/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/ui/scenes/PersonaInfoScene.kt @@ -49,6 +49,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -84,6 +85,7 @@ import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.HorizontalPager import com.google.accompanist.pager.calculateCurrentOffsetForPage import com.google.accompanist.pager.rememberPagerState +import kotlinx.coroutines.launch import org.koin.androidx.compose.get import org.koin.androidx.compose.getViewModel import kotlin.math.absoluteValue @@ -136,6 +138,7 @@ fun PersonaInfoScene( } val context = LocalContext.current + val scope = rememberCoroutineScope() val viewModel: ContactsViewModel = getViewModel() val contactItems by viewModel.items.collectAsState() @@ -256,7 +259,9 @@ fun PersonaInfoScene( .padding(horizontal = 22.5f.dp, vertical = 24.dp) .align(Alignment.BottomCenter), onClose = { - preferenceRepository.setShowContactsTipDialog(false) + scope.launch { + preferenceRepository.setShowContactsTipDialog(false) + } }, text = { Text( diff --git a/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/ui/scenes/avatar/PersonaAvatarModal.kt b/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/ui/scenes/avatar/PersonaAvatarModal.kt index 9b169632..2e3ad98c 100644 --- a/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/ui/scenes/avatar/PersonaAvatarModal.kt +++ b/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/ui/scenes/avatar/PersonaAvatarModal.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -37,6 +38,7 @@ import com.dimension.maskbook.common.ui.widget.button.MaskListItemButton import com.dimension.maskbook.persona.R import com.dimension.maskbook.persona.repository.IPersonaRepository import com.dimension.maskbook.persona.route.PersonaRoute +import kotlinx.coroutines.launch import org.koin.androidx.compose.get @NavGraphDestination( @@ -50,6 +52,7 @@ fun PersonaAvatarModal( @Back onBack: () -> Unit, ) { val repository = get() + val scope = rememberCoroutineScope() MaskModal { Column( verticalArrangement = Arrangement.spacedBy(16.dp) @@ -65,7 +68,9 @@ fun PersonaAvatarModal( ) MaskListItemButton( onClick = { - repository.setAvatarForCurrentPersona(null) + scope.launch { + repository.setAvatarForCurrentPersona(null) + } onBack.invoke() }, text = { diff --git a/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/ui/tab/PersonasTabScreen.kt b/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/ui/tab/PersonasTabScreen.kt index 0bcbf96b..a16fe946 100644 --- a/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/ui/tab/PersonasTabScreen.kt +++ b/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/ui/tab/PersonasTabScreen.kt @@ -23,6 +23,7 @@ package com.dimension.maskbook.persona.ui.tab import android.net.Uri import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope import androidx.navigation.NavController import com.dimension.maskbook.common.ext.navigateToExtension import com.dimension.maskbook.common.route.CommonRoute @@ -35,6 +36,7 @@ import com.dimension.maskbook.persona.repository.IPersonaRepository import com.dimension.maskbook.persona.route.PersonaRoute import com.dimension.maskbook.persona.ui.scenes.PersonaScene import com.dimension.maskbook.persona.ui.scenes.social.connectSocial +import kotlinx.coroutines.launch import org.koin.androidx.compose.get class PersonasTabScreen : TabScreen { @@ -46,6 +48,7 @@ class PersonasTabScreen : TabScreen { @Composable override fun Content(navController: NavController) { val repository = get() + val scope = rememberCoroutineScope() PersonaScene( onBack = { navController.navigateToExtension(null) @@ -84,8 +87,10 @@ class PersonasTabScreen : TabScreen { }, onSocialItemClick = { _, social -> social.network.toPlatform()?.let { - repository.setPlatform(it) - navController.navigateToExtension() + scope.launch { + repository.setPlatform(it) + navController.navigateToExtension() + } } }, onAddPersonaAvatar = { diff --git a/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/viewmodel/RenamePersonaViewModel.kt b/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/viewmodel/RenamePersonaViewModel.kt index 40ac2bc8..0d147d34 100644 --- a/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/viewmodel/RenamePersonaViewModel.kt +++ b/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/viewmodel/RenamePersonaViewModel.kt @@ -48,6 +48,8 @@ class RenamePersonaViewModel( } fun confirm() { - personaRepository.updatePersona(personaId, _name.value) + viewModelScope.launch { + personaRepository.updatePersona(personaId, _name.value) + } } } diff --git a/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/viewmodel/avatar/SetAvatarViewModel.kt b/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/viewmodel/avatar/SetAvatarViewModel.kt index cdaa66a3..3ebb1bb8 100644 --- a/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/viewmodel/avatar/SetAvatarViewModel.kt +++ b/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/viewmodel/avatar/SetAvatarViewModel.kt @@ -22,8 +22,10 @@ package com.dimension.maskbook.persona.viewmodel.avatar import android.net.Uri import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.dimension.maskbook.persona.repository.IPersonaRepository import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch class SetAvatarViewModel( private val repository: IPersonaRepository, @@ -33,6 +35,8 @@ class SetAvatarViewModel( } fun setAvatar(avatar: Uri) { - repository.setAvatarForCurrentPersona(avatar) + viewModelScope.launch { + repository.setAvatarForCurrentPersona(avatar) + } } } diff --git a/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/viewmodel/social/DisconnectSocialViewModel.kt b/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/viewmodel/social/DisconnectSocialViewModel.kt index d5ee172b..4beeb3ec 100644 --- a/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/viewmodel/social/DisconnectSocialViewModel.kt +++ b/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/viewmodel/social/DisconnectSocialViewModel.kt @@ -21,12 +21,16 @@ package com.dimension.maskbook.persona.viewmodel.social import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.dimension.maskbook.persona.repository.IPersonaRepository +import kotlinx.coroutines.launch class DisconnectSocialViewModel( private val repository: IPersonaRepository, ) : ViewModel() { fun disconnectProfile(personaId: String, socialId: String) { - repository.disconnectProfile(personaId, socialId) + viewModelScope.launch { + repository.disconnectProfile(personaId, socialId) + } } } diff --git a/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/viewmodel/social/UserNameModalViewModel.kt b/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/viewmodel/social/UserNameModalViewModel.kt index c9ceb8a6..ac87182b 100644 --- a/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/viewmodel/social/UserNameModalViewModel.kt +++ b/persona/src/androidMain/kotlin/com/dimension/maskbook/persona/viewmodel/social/UserNameModalViewModel.kt @@ -26,6 +26,7 @@ import com.dimension.maskbook.common.ext.asStateIn import com.dimension.maskbook.persona.model.SocialProfile import com.dimension.maskbook.persona.repository.IPersonaRepository import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch class UserNameModalViewModel( private val personaRepository: IPersonaRepository, @@ -40,9 +41,11 @@ class UserNameModalViewModel( } fun done(personaId: String, profileName: String) { - personaRepository.connectProfile( - personaId, - socialProfile.copy(userId = profileName).toString(), - ) + viewModelScope.launch { + personaRepository.connectProfile( + personaId, + socialProfile.copy(userId = profileName).toString(), + ) + } } } diff --git a/setting/export/src/commonMain/kotlin/com/dimension/maskbook/setting/export/SettingServices.kt b/setting/export/src/commonMain/kotlin/com/dimension/maskbook/setting/export/SettingServices.kt index b6b34dfa..3ed2c383 100644 --- a/setting/export/src/commonMain/kotlin/com/dimension/maskbook/setting/export/SettingServices.kt +++ b/setting/export/src/commonMain/kotlin/com/dimension/maskbook/setting/export/SettingServices.kt @@ -29,7 +29,7 @@ interface SettingServices { val paymentPassword: Flow val backupPassword: Flow val shouldShowLegalScene: Flow - fun setBiometricEnabled(value: Boolean) - fun setPaymentPassword(value: String) - fun setShouldShowLegalScene(value: Boolean) + suspend fun setBiometricEnabled(value: Boolean) + suspend fun setPaymentPassword(value: String) + suspend fun setShouldShowLegalScene(value: Boolean) } diff --git a/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/SettingServicesImpl.kt b/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/SettingServicesImpl.kt index 9343e2a6..132ab366 100644 --- a/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/SettingServicesImpl.kt +++ b/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/SettingServicesImpl.kt @@ -48,16 +48,15 @@ class SettingServicesImpl( override val shouldShowLegalScene: Flow get() = settingsRepository.shouldShowLegalScene - - override fun setBiometricEnabled(value: Boolean) { + override suspend fun setBiometricEnabled(value: Boolean) { settingsRepository.setBiometricEnabled(value) } - override fun setPaymentPassword(value: String) { + override suspend fun setPaymentPassword(value: String) { settingsRepository.setPaymentPassword(value) } - override fun setShouldShowLegalScene(value: Boolean) { + override suspend fun setShouldShowLegalScene(value: Boolean) { settingsRepository.setShouldShowLegalScene(value) } diff --git a/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/SettingSetup.kt b/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/SettingSetup.kt index 27f7d78e..0905e517 100644 --- a/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/SettingSetup.kt +++ b/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/SettingSetup.kt @@ -25,6 +25,10 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.navigation import com.dimension.maskbook.common.ModuleSetup +import com.dimension.maskbook.common.di.scope.appScope +import com.dimension.maskbook.common.di.scope.ioDispatcher +import com.dimension.maskbook.common.di.scope.preferenceCoroutineContext +import com.dimension.maskbook.common.di.scope.repositoryCoroutineContext import com.dimension.maskbook.common.retrofit.retrofit import com.dimension.maskbook.common.ui.tab.TabScreen import com.dimension.maskbook.setting.data.JSDataSource @@ -53,10 +57,14 @@ import com.dimension.maskbook.setting.viewmodel.LanguageSettingsViewModel import com.dimension.maskbook.setting.viewmodel.PaymentPasswordSettingsViewModel import com.dimension.maskbook.setting.viewmodel.PhoneBackupViewModel import com.dimension.maskbook.setting.viewmodel.PhoneSetupViewModel +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.core.Koin import org.koin.dsl.bind +import org.koin.dsl.binds import org.koin.dsl.module -import org.koin.mp.KoinPlatformTools object SettingSetup : ModuleSetup { override fun NavGraphBuilder.route(navController: NavController) { @@ -74,14 +82,45 @@ object SettingSetup : ModuleSetup { retrofit("https://vaalh28dbi.execute-api.ap-east-1.amazonaws.com") } single { - SettingsRepository(get(), get(), get(), get()) + SettingsRepository( + get(repositoryCoroutineContext), + get(), + get(), + get(), + get() + ) } - single { BackupRepository(get(), get().cacheDir, get().contentResolver) } - single { SettingServicesImpl(get(), get()) } bind com.dimension.maskbook.setting.export.BackupServices::class + single { + BackupRepository( + get(repositoryCoroutineContext), + get(), + get().cacheDir, + get().contentResolver, + ) + } + single { + SettingServicesImpl( + get(), + get(), + ) + } binds arrayOf( + SettingServices::class, + com.dimension.maskbook.setting.export.BackupServices::class, + ) single { SettingsTabScreen() } bind TabScreen::class - single { JSDataSource(get()) } + single { + JSDataSource( + get(), + get(preferenceCoroutineContext), + ) + } single { JSMethod(get()) } - single { SettingDataSource(get().settingsDataStore) } + single { + SettingDataSource( + get().settingsDataStore, + get(preferenceCoroutineContext), + ) + } viewModel { LanguageSettingsViewModel(get()) } viewModel { AppearanceSettingsViewModel(get()) } @@ -93,14 +132,21 @@ object SettingSetup : ModuleSetup { viewModel { PhoneSetupViewModel(get(), get()) } viewModel { EmailBackupViewModel(get()) } viewModel { PhoneBackupViewModel(get()) } - viewModel { (onDone: () -> Unit, url: String, account: String,) -> - BackupMergeConfirmViewModel(get(), get(), get().contentResolver, onDone, url, account) + viewModel { (onDone: () -> Unit, url: String, account: String) -> + BackupMergeConfirmViewModel( + get(), get(), get().contentResolver, onDone, url, account + ) } viewModel { BackupCloudViewModel(get()) } viewModel { BackupCloudExecuteViewModel(get(), get(), get()) } } - override fun onExtensionReady() { - KoinPlatformTools.defaultContext().get().get().initData() + override fun onExtensionReady(koin: Koin) { + val appScope = koin.get(appScope) + val dispatcher = koin.get(ioDispatcher) + + appScope.launch(dispatcher) { + koin.get().initData() + } } } diff --git a/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/data/JSDataSource.kt b/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/data/JSDataSource.kt index c18dcdd1..478fd3d6 100644 --- a/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/data/JSDataSource.kt +++ b/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/data/JSDataSource.kt @@ -23,18 +23,17 @@ package com.dimension.maskbook.setting.data import com.dimension.maskbook.setting.export.model.Appearance import com.dimension.maskbook.setting.export.model.DataProvider import com.dimension.maskbook.setting.export.model.Language -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlin.coroutines.CoroutineContext internal class JSDataSource( private val jsMethod: JSMethod, + private val preferenceCoroutineContext: CoroutineContext, ) { - private val scope = CoroutineScope(Dispatchers.IO) private val _appearance = MutableStateFlow(Appearance.default) private val _dataProvider = MutableStateFlow(DataProvider.COIN_GECKO) private val _language = MutableStateFlow(Language.auto) @@ -43,29 +42,29 @@ internal class JSDataSource( val appearance = _appearance.asSharedFlow() val dataProvider = _dataProvider.asSharedFlow() - fun setLanguage(language: Language) { - scope.launch { + suspend fun setLanguage(language: Language) { + withContext(preferenceCoroutineContext) { jsMethod.setLanguage(language) _language.value = jsMethod.getLanguage() } } - fun setAppearance(appearance: Appearance) { - scope.launch { + suspend fun setAppearance(appearance: Appearance) { + withContext(preferenceCoroutineContext) { jsMethod.setTheme(appearance) _appearance.value = jsMethod.getTheme() } } - fun setDataProvider(dataProvider: DataProvider) { - scope.launch { + suspend fun setDataProvider(dataProvider: DataProvider) { + withContext(preferenceCoroutineContext) { jsMethod.setTrendingDataSource(dataProvider) _dataProvider.value = jsMethod.getTrendingDataSource() } } - fun initData() { - scope.launch { + suspend fun initData() { + withContext(preferenceCoroutineContext) { awaitAll( async { _language.value = jsMethod.getLanguage() }, async { _appearance.value = jsMethod.getTheme() }, diff --git a/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/data/SettingDataSource.kt b/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/data/SettingDataSource.kt index d3f4f7b2..4368fff1 100644 --- a/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/data/SettingDataSource.kt +++ b/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/data/SettingDataSource.kt @@ -27,11 +27,10 @@ import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlin.coroutines.CoroutineContext private val PaymentPasswordKey = stringPreferencesKey("payment_password") private val BackupPasswordKey = stringPreferencesKey("backup_password") @@ -43,8 +42,8 @@ val Context.settingsDataStore: DataStore by preferencesDataStore(na class SettingDataSource( private val dataStore: DataStore, + private val preferenceCoroutineContext: CoroutineContext, ) { - private val scope = CoroutineScope(Dispatchers.IO) val biometricEnabled: Flow get() = dataStore.data.map { it[BiometricEnabledKey] ?: false @@ -69,48 +68,48 @@ class SettingDataSource( it[RegisterPhone] ?: "" } - fun setPaymentPassword(value: String) { - scope.launch { + suspend fun setPaymentPassword(value: String) { + withContext(preferenceCoroutineContext) { dataStore.edit { it[PaymentPasswordKey] = value } } } - fun setBackupPassword(value: String) { - scope.launch { + suspend fun setBackupPassword(value: String) { + withContext(preferenceCoroutineContext) { dataStore.edit { it[BackupPasswordKey] = value } } } - fun setShouldShowLegalScene(value: Boolean) { - scope.launch { + suspend fun setShouldShowLegalScene(value: Boolean) { + withContext(preferenceCoroutineContext) { dataStore.edit { it[ShouldShowLegalSceneKey] = value } } } - fun setBiometricEnabled(value: Boolean) { - scope.launch { + suspend fun setBiometricEnabled(value: Boolean) { + withContext(preferenceCoroutineContext) { dataStore.edit { it[BiometricEnabledKey] = value } } } - fun setRegisterEmail(value: String) { - scope.launch { + suspend fun setRegisterEmail(value: String) { + withContext(preferenceCoroutineContext) { dataStore.edit { it[RegisterEmail] = value } } } - fun setRegisterPhone(value: String) { - scope.launch { + suspend fun setRegisterPhone(value: String) { + withContext(preferenceCoroutineContext) { dataStore.edit { it[RegisterPhone] = value } diff --git a/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/repository/BackupRepository.kt b/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/repository/BackupRepository.kt index 460faf20..94014581 100644 --- a/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/repository/BackupRepository.kt +++ b/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/repository/BackupRepository.kt @@ -35,9 +35,6 @@ import com.dimension.maskbook.setting.services.model.Scenario import com.dimension.maskbook.setting.services.model.SendCodeBody import com.dimension.maskbook.setting.services.model.UploadBody import com.dimension.maskbook.setting.services.model.ValidateCodeBody -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.withContext import kotlinx.serialization.builtins.serializer import okhttp3.OkHttpClient @@ -52,68 +49,61 @@ import java.util.UUID import javax.crypto.Cipher import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.SecretKeySpec +import kotlin.coroutines.CoroutineContext class BackupRepository( + private val repositoryCoroutineContext: CoroutineContext, private val backupServices: BackupServices, private val cacheDir: File, private val contentResolver: ContentResolver, ) { - private val scope = CoroutineScope(Dispatchers.IO) - suspend fun sendPhoneCode(phone: String) { - withContext(scope.coroutineContext) { - backupServices.sendCode( - SendCodeBody( - account_type = AccountType.phone, - account = phone, - Scenario.backup, - Locale.en, - ) + suspend fun sendPhoneCode(phone: String) = withContext(repositoryCoroutineContext) { + backupServices.sendCode( + SendCodeBody( + account_type = AccountType.phone, + account = phone, + Scenario.backup, + Locale.en, ) - } + ) } - suspend fun sendEmailCode(email: String) { - withContext(scope.coroutineContext) { - backupServices.sendCode( - SendCodeBody( - account_type = AccountType.email, - account = email, - Scenario.backup, - Locale.en, - ) + suspend fun sendEmailCode(email: String) = withContext(repositoryCoroutineContext) { + backupServices.sendCode( + SendCodeBody( + account_type = AccountType.email, + account = email, + Scenario.backup, + Locale.en, ) - } + ) } - suspend fun validatePhoneCode(phone: String, code: String) { - withContext(scope.coroutineContext) { - backupServices.validateCode( - ValidateCodeBody( - code = code, - account = phone, - account_type = AccountType.phone, - ) + suspend fun validatePhoneCode(phone: String, code: String) = withContext(repositoryCoroutineContext) { + backupServices.validateCode( + ValidateCodeBody( + code = code, + account = phone, + account_type = AccountType.phone, ) - } + ) } - suspend fun validateEmailCode(email: String, code: String) { - withContext(scope.coroutineContext) { - backupServices.validateCode( - ValidateCodeBody( - code = code, - account = email, - account_type = AccountType.email, - ) + suspend fun validateEmailCode(email: String, code: String) = withContext(repositoryCoroutineContext) { + backupServices.validateCode( + ValidateCodeBody( + code = code, + account = email, + account_type = AccountType.email, ) - } + ) } suspend fun encryptBackup( password: String, account: String, - content: BackupMetaFile - ): ByteArray = coroutineScope { + content: BackupMetaFile, + ): ByteArray = withContext(repositoryCoroutineContext) { val computedPassword = account.lowercase() + password val iv = SecureRandom().generateSeed(16) val gen = PKCS5S2ParametersGenerator(SHA256Digest()) @@ -131,61 +121,62 @@ class BackupRepository( ).toByteArray() } - suspend fun decryptBackup(password: String, account: String, data: ByteArray): BackupMetaFile = - coroutineScope { - val computedPassword = account.lowercase() + password - val remoteBackupData = RemoteBackupData.fromByteArray(data) - val gen = PKCS5S2ParametersGenerator(SHA256Digest()) - gen.init( - msgPack.encodeToByteArray(String.serializer(), computedPassword), - remoteBackupData.pbkdf2IV, - 10000 - ) - val derivedKey = gen.generateDerivedParameters(256) as KeyParameter - val key = SecretKeySpec(derivedKey.key, "AES") - val cipher = Cipher.getInstance("AES/GCM/NoPadding") - cipher.init(Cipher.DECRYPT_MODE, key, IvParameterSpec(remoteBackupData.paramIV)) - cipher.doFinal(remoteBackupData.encrypted) - .let { msgPack.decodeFromByteArray(BackupMetaFile.serializer(), it) } - } + suspend fun decryptBackup( + password: String, + account: String, + data: ByteArray, + ): BackupMetaFile = withContext(repositoryCoroutineContext) { + val computedPassword = account.lowercase() + password + val remoteBackupData = RemoteBackupData.fromByteArray(data) + val gen = PKCS5S2ParametersGenerator(SHA256Digest()) + gen.init( + msgPack.encodeToByteArray(String.serializer(), computedPassword), + remoteBackupData.pbkdf2IV, + 10000 + ) + val derivedKey = gen.generateDerivedParameters(256) as KeyParameter + val key = SecretKeySpec(derivedKey.key, "AES") + val cipher = Cipher.getInstance("AES/GCM/NoPadding") + cipher.init(Cipher.DECRYPT_MODE, key, IvParameterSpec(remoteBackupData.paramIV)) + cipher.doFinal(remoteBackupData.encrypted) + .let { msgPack.decodeFromByteArray(BackupMetaFile.serializer(), it) } + } - suspend fun downloadBackupWithEmail(email: String, code: String) = - withContext(scope.coroutineContext) { - val response = backupServices.download( - ValidateCodeBody( - code = code, - account = email, - account_type = AccountType.email, - ) - ) - requireNotNull(response.download_url) - BackupFileMeta( - url = downloadFile(response.download_url).toUri().toString(), - size = response.size, - uploaded_at = (response.uploaded_at ?: 0) * 1000, - abstract = response.abstract, + suspend fun downloadBackupWithEmail(email: String, code: String) = withContext(repositoryCoroutineContext) { + val response = backupServices.download( + ValidateCodeBody( + code = code, + account = email, + account_type = AccountType.email, ) - } + ) + requireNotNull(response.download_url) + BackupFileMeta( + url = downloadFile(response.download_url).toUri().toString(), + size = response.size, + uploaded_at = (response.uploaded_at ?: 0) * 1000, + abstract = response.abstract, + ) + } - suspend fun downloadBackupWithPhone(phone: String, code: String) = - withContext(scope.coroutineContext) { - val response = backupServices.download( - ValidateCodeBody( - code = code, - account = phone, - account_type = AccountType.phone, - ) - ) - requireNotNull(response.download_url) - BackupFileMeta( - url = downloadFile(response.download_url).toUri().toString(), - size = response.size, - uploaded_at = (response.uploaded_at ?: 0) * 1000, - abstract = response.abstract, + suspend fun downloadBackupWithPhone(phone: String, code: String) = withContext(repositoryCoroutineContext) { + val response = backupServices.download( + ValidateCodeBody( + code = code, + account = phone, + account_type = AccountType.phone, ) - } + ) + requireNotNull(response.download_url) + BackupFileMeta( + url = downloadFile(response.download_url).toUri().toString(), + size = response.size, + uploaded_at = (response.uploaded_at ?: 0) * 1000, + abstract = response.abstract, + ) + } - private suspend fun downloadFile(url: String) = withContext(scope.coroutineContext) { + private suspend fun downloadFile(url: String) = withContext(repositoryCoroutineContext) { val stream = OkHttpClient.Builder() .build() .newCall( @@ -213,7 +204,7 @@ class BackupRepository( account: String, abstract: String, content: BackupMetaFile, - ) = withContext(scope.coroutineContext) { + ) = withContext(repositoryCoroutineContext) { val response = backupServices.upload( UploadBody( code = code, @@ -234,7 +225,7 @@ class BackupRepository( } suspend fun saveLocality(it: Uri, meta: BackupMetaFile, password: String, account: String) = - coroutineScope { + withContext(repositoryCoroutineContext) { contentResolver.openOutputStream(it)?.use { it.write(encryptBackup(password, account, meta)) } diff --git a/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/repository/ISettingsRepository.kt b/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/repository/ISettingsRepository.kt index 9ca7f557..cf5d8730 100644 --- a/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/repository/ISettingsRepository.kt +++ b/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/repository/ISettingsRepository.kt @@ -37,14 +37,14 @@ interface ISettingsRepository { val shouldShowLegalScene: Flow val email: Flow val phone: Flow - fun setBiometricEnabled(value: Boolean) - fun setLanguage(language: Language) - fun setAppearance(appearance: Appearance) - fun setDataProvider(dataProvider: DataProvider) - fun setPaymentPassword(value: String) - fun setBackupPassword(value: String) + suspend fun setBiometricEnabled(value: Boolean) + suspend fun setLanguage(language: Language) + suspend fun setAppearance(appearance: Appearance) + suspend fun setDataProvider(dataProvider: DataProvider) + suspend fun setPaymentPassword(value: String) + suspend fun setBackupPassword(value: String) suspend fun generateBackupMeta(): BackupMeta - fun provideBackupMeta(file: BackupMetaFile): BackupMeta + suspend fun provideBackupMeta(file: BackupMetaFile): BackupMeta suspend fun restoreBackup(value: BackupMetaFile) suspend fun createBackup( noPosts: Boolean = false, @@ -55,7 +55,7 @@ interface ISettingsRepository { hasPrivateKeyOnly: Boolean = false, ): BackupMetaFile - fun setShouldShowLegalScene(value: Boolean) - fun saveEmail(value: String) - fun savePhone(value: String) + suspend fun setShouldShowLegalScene(value: Boolean) + suspend fun saveEmail(value: String) + suspend fun savePhone(value: String) } diff --git a/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/repository/SettingsRepository.kt b/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/repository/SettingsRepository.kt index 92743522..4bcddd26 100644 --- a/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/repository/SettingsRepository.kt +++ b/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/repository/SettingsRepository.kt @@ -40,11 +40,12 @@ import com.dimension.maskbook.setting.model.mapping.toIndexedDBProfile import com.dimension.maskbook.setting.model.mapping.toIndexedDBRelation import com.dimension.maskbook.setting.model.mapping.toWalletData import com.dimension.maskbook.wallet.export.WalletServices -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.withContext +import kotlin.coroutines.CoroutineContext internal class SettingsRepository( + private val repositoryCoroutineContext: CoroutineContext, private val personaServices: PersonaServices, private val settingDataSource: SettingDataSource, private val jsDataSource: JSDataSource, @@ -69,36 +70,36 @@ internal class SettingsRepository( override val phone: Flow get() = settingDataSource.phone - override fun setBiometricEnabled(value: Boolean) { + override suspend fun setBiometricEnabled(value: Boolean) = withContext(repositoryCoroutineContext) { settingDataSource.setBiometricEnabled(value) } - override fun setLanguage(language: Language) { + override suspend fun setLanguage(language: Language) = withContext(repositoryCoroutineContext) { jsDataSource.setLanguage(language) } - override fun setAppearance(appearance: Appearance) { + override suspend fun setAppearance(appearance: Appearance) = withContext(repositoryCoroutineContext) { jsDataSource.setAppearance(appearance) } - override fun setDataProvider(dataProvider: DataProvider) { + override suspend fun setDataProvider(dataProvider: DataProvider) = withContext(repositoryCoroutineContext) { jsDataSource.setDataProvider(dataProvider) } - override fun setPaymentPassword(value: String) { + override suspend fun setPaymentPassword(value: String) = withContext(repositoryCoroutineContext) { settingDataSource.setPaymentPassword(value) } - override fun setBackupPassword(value: String) { + override suspend fun setBackupPassword(value: String) = withContext(repositoryCoroutineContext) { settingDataSource.setBackupPassword(value) } - override fun setShouldShowLegalScene(value: Boolean) { + override suspend fun setShouldShowLegalScene(value: Boolean) = withContext(repositoryCoroutineContext) { settingDataSource.setShouldShowLegalScene(value) } - override suspend fun generateBackupMeta(): BackupMeta { - return createBackup( + override suspend fun generateBackupMeta(): BackupMeta = withContext(repositoryCoroutineContext) { + createBackup( noPosts = false, noWallets = false, noPersonas = false, @@ -109,8 +110,10 @@ internal class SettingsRepository( } } - override fun provideBackupMeta(file: BackupMetaFile): BackupMeta { - return BackupMeta( + override suspend fun provideBackupMeta( + file: BackupMetaFile, + ): BackupMeta = withContext(repositoryCoroutineContext) { + BackupMeta( personas = file.personas.size, associatedAccount = file.personas.sumOf { it.linkedProfiles.size }, encryptedPost = file.posts.size, @@ -122,19 +125,17 @@ internal class SettingsRepository( ) } - override suspend fun restoreBackup(value: BackupMetaFile) { - withContext(Dispatchers.IO) { - val persona = value.personas.map { it.toIndexedDBPersona() } - personaServices.restorePersonaBackup(persona) - val profile = value.profiles.map { it.toIndexedDBProfile() } - personaServices.restoreProfileBackup(profile) - val relation = value.relations.map { it.toIndexedDBRelation() } - personaServices.restoreRelationBackup(relation) - val post = value.posts.map { it.toIndexDbPost() } - personaServices.restorePostBackup(post) - val wallet = value.wallets.map { it.toWalletData() } - walletServices.restoreWalletBackup(wallet) - } + override suspend fun restoreBackup(value: BackupMetaFile) = withContext(repositoryCoroutineContext) { + val persona = value.personas.map { it.toIndexedDBPersona() } + personaServices.restorePersonaBackup(persona) + val profile = value.profiles.map { it.toIndexedDBProfile() } + personaServices.restoreProfileBackup(profile) + val relation = value.relations.map { it.toIndexedDBRelation() } + personaServices.restoreRelationBackup(relation) + val post = value.posts.map { it.toIndexDbPost() } + personaServices.restorePostBackup(post) + val wallet = value.wallets.map { it.toWalletData() } + walletServices.restoreWalletBackup(wallet) } override suspend fun createBackup( @@ -144,80 +145,80 @@ internal class SettingsRepository( noProfiles: Boolean, noRelations: Boolean, hasPrivateKeyOnly: Boolean - ): BackupMetaFile { - return withContext(Dispatchers.IO) { - val personas = if (noPersonas) { - emptyList() - } else { - backupPersona(hasPrivateKeyOnly) - } - val profile = if (noProfiles) { - emptyList() - } else { - backProfiles() - } - val wallets = if (noWallets) { - emptyList() - } else { - backupWallets() - } - val posts = if (noPosts) { - emptyList() - } else { - backupPosts() - } - val relations = if (noRelations) { - emptyList() - } else { - backupRelations() - } - BackupMetaFile( - personas = personas, - wallets = wallets, - posts = posts, - profiles = profile, - meta = BackupMetaFile.Meta.Default, - grantedHostPermissions = emptyList(), - relations = relations, - ) + ): BackupMetaFile = withContext(repositoryCoroutineContext) { + val personas = if (noPersonas) { + emptyList() + } else { + backupPersona(hasPrivateKeyOnly) + } + val profile = if (noProfiles) { + emptyList() + } else { + backProfiles() + } + val wallets = if (noWallets) { + emptyList() + } else { + backupWallets() + } + val posts = if (noPosts) { + emptyList() + } else { + backupPosts() } + val relations = if (noRelations) { + emptyList() + } else { + backupRelations() + } + BackupMetaFile( + personas = personas, + wallets = wallets, + posts = posts, + profiles = profile, + meta = BackupMetaFile.Meta.Default, + grantedHostPermissions = emptyList(), + relations = relations, + ) } - private suspend fun backupRelations(): List { - return personaServices.createRelationsBackup().map { + private suspend fun backupRelations(): List = withContext(repositoryCoroutineContext) { + personaServices.createRelationsBackup().map { it.toBackupRelation() } } - private suspend fun backupPosts(): List { - return personaServices.createPostsBackup().map { + private suspend fun backupPosts(): List = withContext(repositoryCoroutineContext) { + personaServices.createPostsBackup().map { it.toBackupPost() } } - private suspend fun backupWallets(): List { - return walletServices.createWalletBackup().map { + private suspend fun backupWallets(): List = withContext(repositoryCoroutineContext) { + walletServices.createWalletBackup().map { it.toBackupWallet() } } - private suspend fun backProfiles(): List { - return personaServices.createProfileBackup().map { + private suspend fun backProfiles(): List = withContext(repositoryCoroutineContext) { + personaServices.createProfileBackup().map { it.toBackupProfile() } } - private suspend fun backupPersona(hasPrivateKeyOnly: Boolean): List { - return personaServices.createPersonaBackup(hasPrivateKeyOnly).map { + private suspend fun backupPersona( + hasPrivateKeyOnly: Boolean + ): List = withContext(repositoryCoroutineContext) { + personaServices.createPersonaBackup(hasPrivateKeyOnly).map { it.toBackupPersona() } } - override fun saveEmail(value: String) { + override suspend fun saveEmail(value: String) = withContext(repositoryCoroutineContext) { settingDataSource.setRegisterEmail(value) } - override fun savePhone(value: String) { + override suspend fun savePhone(value: String) = withContext(repositoryCoroutineContext) { settingDataSource.setRegisterPhone(value) } } diff --git a/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/ui/scenes/SettingsScene.kt b/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/ui/scenes/SettingsScene.kt index 02ea686f..1aee9a4f 100644 --- a/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/ui/scenes/SettingsScene.kt +++ b/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/ui/scenes/SettingsScene.kt @@ -175,7 +175,6 @@ fun SettingsScene( !biometricEnabled, context, biometricEnableViewModel, - repository ) }) }, @@ -184,7 +183,6 @@ fun SettingsScene( !biometricEnabled, context, biometricEnableViewModel, - repository ) }, ) @@ -291,7 +289,6 @@ private fun enableBiometric( enable: Boolean, context: Context, viewModel: BiometricEnableViewModel, - repository: ISettingsRepository ) { if (enable) { viewModel.enable( @@ -300,7 +297,7 @@ private fun enableBiometric( negativeButton = R.string.common_controls_cancel, ) } else { - repository.setBiometricEnabled(enable) + viewModel.setBiometricEnabled(false) } } diff --git a/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/viewmodel/AppearanceSettingsViewModel.kt b/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/viewmodel/AppearanceSettingsViewModel.kt index 67a2bd78..8e4a38c0 100644 --- a/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/viewmodel/AppearanceSettingsViewModel.kt +++ b/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/viewmodel/AppearanceSettingsViewModel.kt @@ -25,6 +25,7 @@ import androidx.lifecycle.viewModelScope import com.dimension.maskbook.common.ext.asStateIn import com.dimension.maskbook.setting.export.model.Appearance import com.dimension.maskbook.setting.repository.ISettingsRepository +import kotlinx.coroutines.launch class AppearanceSettingsViewModel( private val repository: ISettingsRepository, @@ -34,6 +35,8 @@ class AppearanceSettingsViewModel( } fun setAppearance(appearance: Appearance) { - repository.setAppearance(appearance = appearance) + viewModelScope.launch { + repository.setAppearance(appearance = appearance) + } } } diff --git a/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/viewmodel/BackupPasswordSettingsViewModel.kt b/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/viewmodel/BackupPasswordSettingsViewModel.kt index f5055385..ef620752 100644 --- a/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/viewmodel/BackupPasswordSettingsViewModel.kt +++ b/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/viewmodel/BackupPasswordSettingsViewModel.kt @@ -27,6 +27,7 @@ import com.dimension.maskbook.common.ext.asStateIn import com.dimension.maskbook.setting.repository.ISettingsRepository import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch class BackupPasswordSettingsViewModel( private val repository: ISettingsRepository, @@ -93,6 +94,8 @@ class BackupPasswordSettingsViewModel( } fun confirm() { - repository.setBackupPassword(newPassword.value) + viewModelScope.launch { + repository.setBackupPassword(newPassword.value) + } } } diff --git a/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/viewmodel/DataSourceSettingsViewModel.kt b/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/viewmodel/DataSourceSettingsViewModel.kt index 68ef5041..a6725d56 100644 --- a/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/viewmodel/DataSourceSettingsViewModel.kt +++ b/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/viewmodel/DataSourceSettingsViewModel.kt @@ -25,6 +25,7 @@ import androidx.lifecycle.viewModelScope import com.dimension.maskbook.common.ext.asStateIn import com.dimension.maskbook.setting.export.model.DataProvider import com.dimension.maskbook.setting.repository.ISettingsRepository +import kotlinx.coroutines.launch class DataSourceSettingsViewModel( private val repository: ISettingsRepository, @@ -34,6 +35,8 @@ class DataSourceSettingsViewModel( } fun setDataProvider(dataProvider: DataProvider) { - repository.setDataProvider(dataProvider = dataProvider) + viewModelScope.launch { + repository.setDataProvider(dataProvider = dataProvider) + } } } diff --git a/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/viewmodel/LanguageSettingsViewModel.kt b/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/viewmodel/LanguageSettingsViewModel.kt index d787629a..5d99a971 100644 --- a/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/viewmodel/LanguageSettingsViewModel.kt +++ b/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/viewmodel/LanguageSettingsViewModel.kt @@ -25,6 +25,7 @@ import androidx.lifecycle.viewModelScope import com.dimension.maskbook.common.ext.asStateIn import com.dimension.maskbook.setting.export.model.Language import com.dimension.maskbook.setting.repository.ISettingsRepository +import kotlinx.coroutines.launch class LanguageSettingsViewModel( private val repository: ISettingsRepository @@ -34,6 +35,8 @@ class LanguageSettingsViewModel( } fun setLanguage(language: Language) { - repository.setLanguage(language = language) + viewModelScope.launch { + repository.setLanguage(language = language) + } } } diff --git a/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/viewmodel/PaymentPasswordSettingsViewModel.kt b/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/viewmodel/PaymentPasswordSettingsViewModel.kt index 1ea76091..4eedae3d 100644 --- a/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/viewmodel/PaymentPasswordSettingsViewModel.kt +++ b/setting/src/androidMain/kotlin/com/dimension/maskbook/setting/viewmodel/PaymentPasswordSettingsViewModel.kt @@ -27,6 +27,7 @@ import com.dimension.maskbook.common.ext.asStateIn import com.dimension.maskbook.setting.repository.ISettingsRepository import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch class PaymentPasswordSettingsViewModel( private val repository: ISettingsRepository, @@ -89,6 +90,8 @@ class PaymentPasswordSettingsViewModel( } fun confirm() { - repository.setPaymentPassword(newPassword.value) + viewModelScope.launch { + repository.setPaymentPassword(newPassword.value) + } } } diff --git a/wallet/src/androidMain/kotlin/com/dimension/maskbook/wallet/WalletSetup.kt b/wallet/src/androidMain/kotlin/com/dimension/maskbook/wallet/WalletSetup.kt index af3d64e3..f7cd608f 100644 --- a/wallet/src/androidMain/kotlin/com/dimension/maskbook/wallet/WalletSetup.kt +++ b/wallet/src/androidMain/kotlin/com/dimension/maskbook/wallet/WalletSetup.kt @@ -26,6 +26,8 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.room.Room import com.dimension.maskbook.common.ModuleSetup +import com.dimension.maskbook.common.di.scope.appScope +import com.dimension.maskbook.common.di.scope.ioDispatcher import com.dimension.maskbook.common.ui.tab.TabScreen import com.dimension.maskbook.wallet.data.JSMethod import com.dimension.maskbook.wallet.db.AppDatabase @@ -75,7 +77,6 @@ import com.dimension.maskbook.wallet.usecase.SendTransactionUseCase import com.dimension.maskbook.wallet.usecase.SendWalletCollectibleUseCase import com.dimension.maskbook.wallet.usecase.SetCurrentChainUseCase import com.dimension.maskbook.wallet.usecase.VerifyPaymentPasswordUseCase -import com.dimension.maskbook.wallet.viewmodel.WelcomeViewModel import com.dimension.maskbook.wallet.viewmodel.wallets.TokenDetailViewModel import com.dimension.maskbook.wallet.viewmodel.wallets.TouchIdEnableViewModel import com.dimension.maskbook.wallet.viewmodel.wallets.UnlockWalletViewModel @@ -109,15 +110,15 @@ import com.dimension.maskbook.wallet.walletconnect.WalletConnectServerManager import com.dimension.maskbook.wallet.walletconnect.v1.client.WalletConnectClientManagerV1 import com.dimension.maskbook.wallet.walletconnect.v1.server.WalletConnectServerManagerV1 import com.google.accompanist.navigation.animation.navigation +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.asExecutor import kotlinx.coroutines.launch import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.core.Koin import org.koin.core.module.Module import org.koin.dsl.bind import org.koin.dsl.module -import org.koin.mp.KoinPlatformTools import com.dimension.maskbook.wallet.export.WalletServices as ExportWalletServices object WalletSetup : ModuleSetup { @@ -141,9 +142,10 @@ object WalletSetup : ModuleSetup { override fun dependencyInject() = module { single { + val executor = get(ioDispatcher).asExecutor() Room.databaseBuilder(get(), AppDatabase::class.java, "maskbook") - .setQueryExecutor(Dispatchers.IO.asExecutor()) - .setTransactionExecutor(Dispatchers.IO.asExecutor()) + .setQueryExecutor(executor) + .setTransactionExecutor(executor) .addMigrations( RoomMigrations.MIGRATION_6_7, RoomMigrations.MIGRATION_7_8, @@ -162,59 +164,69 @@ object WalletSetup : ModuleSetup { provideServices() } - override fun onExtensionReady() { - initRepository() - initWalletConnect() - initEvent() + override fun onExtensionReady(koin: Koin) { + initRepository(koin) + initWalletConnect(koin) + initEvent(koin) } } -private fun initEvent() { - with(KoinPlatformTools.defaultContext().get()) { - CoroutineScope(Dispatchers.IO).launch { - launch { - get().web3Event().collect { - get().handle(it) +private fun initEvent(koin: Koin) { + val appScope = koin.get(appScope) + val ioDispatcher = koin.get(ioDispatcher) + val jsMethod = koin.get() + val walletRepository = koin.get() + + appScope.launch(ioDispatcher) { + jsMethod.web3Event().collect { + koin.get().handle(it) + } + } + + appScope.launch(ioDispatcher) { + jsMethod.switchBlockChain().collect { data -> + if (data.coinId != null) { + val platform = CoinPlatformType.values().firstOrNull { it.coinId == data.coinId } + if (platform != null) { + walletRepository.setActiveCoinPlatformType(platform) } } - launch { - get().switchBlockChain().collect { data -> - if (data.coinId != null) { - val platform = - CoinPlatformType.values().firstOrNull { it.coinId == data.coinId } - if (platform != null) { - get().setActiveCoinPlatformType(platform) - } - } - if (data.networkId != null) { - val chainType = - ChainType.values().firstOrNull { it.chainId == data.networkId } - if (chainType != null) { - get().setChainType(chainType, false) - } - } + if (data.networkId != null) { + val chainType = ChainType.values().firstOrNull { it.chainId == data.networkId } + if (chainType != null) { + walletRepository.setChainType(chainType, false) } } } } } -private fun initRepository() { - KoinPlatformTools.defaultContext().get().get().init() - KoinPlatformTools.defaultContext().get().get().init() +private fun initRepository(koin: Koin) { + val scope = koin.get(appScope) + val dispatcher = koin.get(ioDispatcher) + + scope.launch(dispatcher) { + koin.get().init() + } + scope.launch(dispatcher) { + koin.get().init() + } } -private fun initWalletConnect() { - val walletRepository = KoinPlatformTools.defaultContext().get().get() - KoinPlatformTools.defaultContext().get().get() +private fun initWalletConnect(koin: Koin) { + val appScope = koin.get(appScope) + val dispatcher = koin.get(ioDispatcher) + val walletRepository = koin.get() + + koin.get() .initSessions { address -> - CoroutineScope(Dispatchers.IO).launch { + appScope.launch(dispatcher) { walletRepository.findWalletByAddress(address)?.let { wallet -> walletRepository.deleteWallet(wallet.id) } } } - KoinPlatformTools.defaultContext().get().get() + koin.get() .init { _, _ -> // clientMeta, request -> TODO("navigate to wallet connect request handle scene") } @@ -231,10 +243,12 @@ private fun Module.provideRepository() { single { WalletRepository( get().walletDataStore, + get(appScope), + get(ioDispatcher), + get(), get(), get(), get(), - get() ) } single { JSMethod(get()) } @@ -242,8 +256,8 @@ private fun Module.provideRepository() { single { CollectibleRepository(get(), get()) } single { TransactionRepository(get(), get()) } single { TokenRepository(get()) } - single { SendHistoryRepository(get()) } - single { WalletContactRepository(get()) } + single { SendHistoryRepository(get(), get(ioDispatcher)) } + single { WalletContactRepository(get(), get(ioDispatcher)) } single { WalletConnectRepository(get(), get()) } } @@ -283,7 +297,6 @@ private fun Module.provideUseCase() { } private fun Module.provideViewModel() { - viewModel { WelcomeViewModel(get()) } viewModel { (wallet: String) -> CreateWalletRecoveryKeyViewModel(wallet, get()) } viewModel { TouchIdEnableViewModel() } viewModel { (wallet: String) -> ImportWalletKeystoreViewModel(wallet, get()) } diff --git a/wallet/src/androidMain/kotlin/com/dimension/maskbook/wallet/handler/Web3MessageHandler.kt b/wallet/src/androidMain/kotlin/com/dimension/maskbook/wallet/handler/Web3MessageHandler.kt index 2abeb4f0..9ed7390a 100644 --- a/wallet/src/androidMain/kotlin/com/dimension/maskbook/wallet/handler/Web3MessageHandler.kt +++ b/wallet/src/androidMain/kotlin/com/dimension/maskbook/wallet/handler/Web3MessageHandler.kt @@ -36,148 +36,145 @@ import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.node.ArrayNode import com.fasterxml.jackson.databind.node.NullNode import com.fasterxml.jackson.databind.node.ObjectNode -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.launch import org.web3j.protocol.core.Request import org.web3j.protocol.core.Response internal class Web3MessageHandler( private val walletRepository: IWalletRepository, ) { - private val scope = CoroutineScope(Dispatchers.IO) - fun handle(request: Web3Request) { - scope.launch { - val payload = request.payload - if (payload?.id != null) { - when (payload.method) { - "getRPCurl" -> { - walletRepository.dWebData.firstOrNull()?.chainType?.endpoint?.let { - request.message.response( - Web3SendResponse.success(request, mapOf("rpcURL" to it)) - ) - } - } - "eth_coinbase" -> { - val address = walletRepository.currentWallet.firstOrNull()?.address ?: "" + suspend fun handle(request: Web3Request) { + val payload = request.payload + if (payload?.id != null) { + when (payload.method) { + "getRPCurl" -> { + walletRepository.dWebData.firstOrNull()?.chainType?.endpoint?.let { request.message.response( - Web3SendResponse.success( - request, - mapOf("coinbase" to address) - ) + Web3SendResponse.success(request, mapOf("rpcURL" to it)) ) } - "eth_getAccounts", "eth_accounts" -> { - val address = walletRepository.currentWallet.firstOrNull()?.address - request.message.response( - Web3SendResponse.success( - request, - listOfNotNull(address) - ) + } + "eth_coinbase" -> { + val address = walletRepository.currentWallet.firstOrNull()?.address ?: "" + request.message.response( + Web3SendResponse.success( + request, + mapOf("coinbase" to address) ) - } - "eth_sendTransaction" -> { - val dataRaw = payload.params.firstOrNull() - ?.decodeJson() - ?.encodeJson() ?: return@launch - val requestRaw = SendTokenRequest( - messageId = request.id, - payloadId = request.payload.id, - jsonrpc = request.payload.jsonrpc, - ).encodeJson() - Navigator.navigate(WalletRoute.SendTokenConfirm(dataRaw, requestRaw)) - } - "personal_sign" -> { - val message = payload.params.getOrNull(0)?.normalized as? String - ?: return@launch - val fromAddress = payload.params.getOrNull(1)?.normalized as? String - ?: return@launch - val hex = walletRepository.signMessage(message, fromAddress) - ?: return@launch - request.message.response( - Web3SendResponse.success( - request, - listOfNotNull(hex) - ) + ) + } + "eth_getAccounts", "eth_accounts" -> { + val address = walletRepository.currentWallet.firstOrNull()?.address + request.message.response( + Web3SendResponse.success( + request, + listOfNotNull(address) ) - } - else -> { - val method = payload.method - val chainType = - walletRepository.dWebData.firstOrNull()?.chainType ?: return@launch - val service = chainType.httpService - try { - val response = service.send( - Request( - method, - payload.params.map { it.normalized }, - service, - JsonResponse::class.java - ), + ) + } + "eth_sendTransaction" -> { + val dataRaw = payload.params.firstOrNull() + ?.decodeJson() + ?.encodeJson() ?: return + val requestRaw = SendTokenRequest( + messageId = request.id, + payloadId = request.payload.id, + jsonrpc = request.payload.jsonrpc, + ).encodeJson() + Navigator.navigate(WalletRoute.SendTokenConfirm(dataRaw, requestRaw)) + } + "personal_sign" -> { + val message = payload.params.getOrNull(0)?.normalized as? String + ?: return + val fromAddress = payload.params.getOrNull(1)?.normalized as? String + ?: return + val hex = walletRepository.signMessage(message, fromAddress) + ?: return + request.message.response( + Web3SendResponse.success( + request, + listOfNotNull(hex) + ) + ) + } + else -> { + val method = payload.method + val chainType = + walletRepository.dWebData.firstOrNull()?.chainType ?: return + val service = chainType.httpService + try { + val response = service.send( + Request( + method, + payload.params.map { it.normalized }, + service, JsonResponse::class.java - ) - val result = response?.result - if (result == null) { - request.message.response( - Web3SendResponse.error( - request, - "No response" - ) - ) - } else { - request.message.response( - Web3SendResponse.success( - request, - when (result) { - is NullNode -> null - is ObjectNode -> ObjectMapper().convertValue( - result, - object : TypeReference>() {}, - ) - is ArrayNode -> ObjectMapper().convertValue( - result, - object : TypeReference>() {}, - ) - else -> result.asText() - } - ) - ) - } - } catch (e: Throwable) { - e.printStackTrace() + ), + JsonResponse::class.java + ) + val result = response?.result + if (result == null) { request.message.response( Web3SendResponse.error( request, - e.message ?: "error" + "No response" + ) + ) + } else { + request.message.response( + Web3SendResponse.success( + request, + when (result) { + is NullNode -> null + is ObjectNode -> ObjectMapper().convertValue( + result, + object : TypeReference>() {}, + ) + is ArrayNode -> ObjectMapper().convertValue( + result, + object : TypeReference>() {}, + ) + else -> result.asText() + } ) ) } + } catch (e: Throwable) { + e.printStackTrace() + request.message.response( + Web3SendResponse.error( + request, + e.message ?: "error" + ) + ) } } } } } -} -private inline fun Web3SendResponse.Companion.success(request: Web3Request, result: T?): Map { - requireNotNull(request.payload) - return success( - messageId = request.id, - jsonrpc = request.payload.jsonrpc, - payloadId = request.payload.id, - result = result - ) -} + private inline fun Web3SendResponse.Companion.success( + request: Web3Request, + result: T? + ): Map { + requireNotNull(request.payload) + return success( + messageId = request.id, + jsonrpc = request.payload.jsonrpc, + payloadId = request.payload.id, + result = result + ) + } -private fun Web3SendResponse.Companion.error(request: Web3Request, error: String): Map { - requireNotNull(request.payload) - return error( - messageId = request.id, - jsonrpc = request.payload.jsonrpc, - payloadId = request.payload.id, - error = error - ) -} + private fun Web3SendResponse.Companion.error(request: Web3Request, error: String): Map { + requireNotNull(request.payload) + return error( + messageId = request.id, + jsonrpc = request.payload.jsonrpc, + payloadId = request.payload.id, + error = error + ) + } -class JsonResponse : Response() + class JsonResponse : Response() +} diff --git a/wallet/src/androidMain/kotlin/com/dimension/maskbook/wallet/repository/ISendHistoryRepository.kt b/wallet/src/androidMain/kotlin/com/dimension/maskbook/wallet/repository/ISendHistoryRepository.kt index 0ed27409..4d9d89be 100644 --- a/wallet/src/androidMain/kotlin/com/dimension/maskbook/wallet/repository/ISendHistoryRepository.kt +++ b/wallet/src/androidMain/kotlin/com/dimension/maskbook/wallet/repository/ISendHistoryRepository.kt @@ -22,8 +22,7 @@ package com.dimension.maskbook.wallet.repository import com.dimension.maskbook.wallet.db.AppDatabase import com.dimension.maskbook.wallet.db.model.DbSendHistory -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flatMapLatest @@ -41,8 +40,8 @@ interface ISendHistoryRepository { class SendHistoryRepository( private val database: AppDatabase, + private val dispatcher: CoroutineDispatcher, ) : ISendHistoryRepository { - private val scope = CoroutineScope(Dispatchers.IO) override val recent: Flow> get() = database.sendHistoryDao().getAll().map { list -> list.sortedByDescending { it.history.lastSend } @@ -50,7 +49,7 @@ class SendHistoryRepository( } override suspend fun addOrUpdate(address: String, name: String) { - withContext(scope.coroutineContext) { + withContext(dispatcher) { with(database.sendHistoryDao()) { val currentTime = System.currentTimeMillis() if (contains(address) > 0) { diff --git a/wallet/src/androidMain/kotlin/com/dimension/maskbook/wallet/repository/IWalletConnectRepository.kt b/wallet/src/androidMain/kotlin/com/dimension/maskbook/wallet/repository/IWalletConnectRepository.kt index d2c7e84b..7ce3b25c 100644 --- a/wallet/src/androidMain/kotlin/com/dimension/maskbook/wallet/repository/IWalletConnectRepository.kt +++ b/wallet/src/androidMain/kotlin/com/dimension/maskbook/wallet/repository/IWalletConnectRepository.kt @@ -33,13 +33,10 @@ import com.dimension.maskbook.wallet.export.model.ChainType import com.dimension.maskbook.wallet.services.WalletServices import com.dimension.maskbook.wallet.services.model.WCSupportedWallet import com.dimension.maskbook.wallet.walletconnect.WCResponder -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch import java.util.UUID data class WCWallet( @@ -90,27 +87,24 @@ data class WCWallet( interface IWalletConnectRepository { val supportedWallets: Flow> - fun init() + suspend fun init() // returns id of first wallet suspend fun saveAccounts(responder: WCResponder, platformType: CoinPlatformType): String? } class WalletConnectRepository( private val walletServices: WalletServices, - private val database: AppDatabase + private val database: AppDatabase, ) : IWalletConnectRepository { - private val wcScope = CoroutineScope(Dispatchers.IO) - override fun init() { - wcScope.launch { - try { - refreshSupportedWallets() - } catch (e: Throwable) { - if (BuildConfig.DEBUG) e.printStackTrace() - // retry - delay(30000) - refreshSupportedWallets() - } + override suspend fun init() { + try { + refreshSupportedWallets() + } catch (e: Throwable) { + if (BuildConfig.DEBUG) e.printStackTrace() + // retry + delay(30000) + refreshSupportedWallets() } } diff --git a/wallet/src/androidMain/kotlin/com/dimension/maskbook/wallet/repository/IWalletRepository.kt b/wallet/src/androidMain/kotlin/com/dimension/maskbook/wallet/repository/IWalletRepository.kt index d7647b87..e1eeeaf8 100644 --- a/wallet/src/androidMain/kotlin/com/dimension/maskbook/wallet/repository/IWalletRepository.kt +++ b/wallet/src/androidMain/kotlin/com/dimension/maskbook/wallet/repository/IWalletRepository.kt @@ -159,7 +159,7 @@ data class DWebData( ) interface IWalletRepository { - fun init() + suspend fun init() val dWebData: Flow fun setActiveCoinPlatformType(platformType: CoinPlatformType) fun setChainType(networkType: ChainType, notifyJS: Boolean = true) diff --git a/wallet/src/androidMain/kotlin/com/dimension/maskbook/wallet/repository/WalletContactRepository.kt b/wallet/src/androidMain/kotlin/com/dimension/maskbook/wallet/repository/WalletContactRepository.kt index b4aa3b3f..66d66086 100644 --- a/wallet/src/androidMain/kotlin/com/dimension/maskbook/wallet/repository/WalletContactRepository.kt +++ b/wallet/src/androidMain/kotlin/com/dimension/maskbook/wallet/repository/WalletContactRepository.kt @@ -23,8 +23,7 @@ package com.dimension.maskbook.wallet.repository import com.dimension.maskbook.wallet.db.AppDatabase import com.dimension.maskbook.wallet.db.model.DbSendHistoryWithContact import com.dimension.maskbook.wallet.db.model.DbWalletContact -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext @@ -63,13 +62,14 @@ interface IWalletContactRepository { class WalletContactRepository( private val database: AppDatabase, + private val dispatcher: CoroutineDispatcher, ) : IWalletContactRepository { - private val scope = CoroutineScope(Dispatchers.IO) + override val contacts: Flow> get() = database.walletContactDao().getAll().map { it.map { SearchAddressData.fromDb(it) } } override suspend fun addOrUpdate(address: String, name: String) { - withContext(scope.coroutineContext) { + withContext(dispatcher) { val item = database.walletContactDao().getByAddress(address = address)?.copy(name = name) ?: DbWalletContact(UUID.randomUUID().toString(), name, address) database.walletContactDao().add(listOf(item)) diff --git a/wallet/src/androidMain/kotlin/com/dimension/maskbook/wallet/repository/WalletRepository.kt b/wallet/src/androidMain/kotlin/com/dimension/maskbook/wallet/repository/WalletRepository.kt index 3b9ecb52..0bc10e23 100644 --- a/wallet/src/androidMain/kotlin/com/dimension/maskbook/wallet/repository/WalletRepository.kt +++ b/wallet/src/androidMain/kotlin/com/dimension/maskbook/wallet/repository/WalletRepository.kt @@ -66,8 +66,8 @@ import com.dimension.maskbook.wallet.services.WalletServices import com.dimension.maskbook.wallet.walletconnect.WalletConnectClientManager import com.dimension.maskwalletcore.CoinType import com.dimension.maskwalletcore.WalletKey +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow @@ -91,7 +91,6 @@ import java.math.BigInteger import java.util.UUID import kotlin.math.pow import kotlin.time.Duration.Companion.seconds -import kotlin.time.ExperimentalTime private val CurrentCoinPlatformTypeKey = stringPreferencesKey("coin_platform_type") private val CurrentWalletKey = stringPreferencesKey("current_wallet") @@ -111,22 +110,19 @@ private fun Token.toDbToken(chainId: ChainID?) = DbToken( internal class WalletRepository( private val dataStore: DataStore, + private val appScope: CoroutineScope, + private val dispatcher: CoroutineDispatcher, private val database: AppDatabase, private val services: WalletServices, private val walletConnectManager: WalletConnectClientManager, private val jsMethod: JSMethod, ) : IWalletRepository { - private val tokenScope = CoroutineScope(Dispatchers.IO) - private val scope = CoroutineScope(Dispatchers.IO) - - @OptIn(ExperimentalTime::class) - override fun init() { - tokenScope.launch { - refreshChainData() - while (true) { - delay(12.seconds) - refreshWallet() - } + + override suspend fun init() { + refreshChainData() + while (true) { + delay(12.seconds) + refreshWallet() } } @@ -143,7 +139,7 @@ internal class WalletRepository( } override fun setActiveCoinPlatformType(platformType: CoinPlatformType) { - scope.launch { + appScope.launch(dispatcher) { dataStore.edit { it[CurrentCoinPlatformTypeKey] = platformType.name } @@ -151,7 +147,7 @@ internal class WalletRepository( } override fun setChainType(networkType: ChainType, notifyJS: Boolean) { - scope.launch { + appScope.launch(dispatcher) { dataStore.edit { it[ChainTypeKey] = networkType.name } @@ -350,7 +346,7 @@ internal class WalletRepository( } override fun setCurrentWallet(walletId: String) { - scope.launch { + appScope.launch(dispatcher) { database.walletDao().getById(walletId)?.let { setCurrentWallet(it.wallet) } @@ -358,7 +354,7 @@ internal class WalletRepository( } fun setCurrentWallet(dbWallet: DbWallet?) { - scope.launch { + appScope.launch(dispatcher) { dataStore.edit { it[CurrentWalletKey] = dbWallet?.id.orEmpty() } @@ -413,7 +409,7 @@ internal class WalletRepository( path: List, platformType: CoinPlatformType, ) { - scope.launch { + appScope.launch(dispatcher) { val wallet = WalletKey.fromMnemonic(mnemonic = mnemonicCode.joinToString(" "), "") val accounts = path.map { wallet.addNewAccountAtPath(platformType.coinType, it, name, "") @@ -495,7 +491,7 @@ internal class WalletRepository( privateKey: String, platformType: CoinPlatformType, ) { - scope.launch { + appScope.launch(dispatcher) { val wallet = WalletKey.fromPrivateKey( privateKey = privateKey, name = name, @@ -576,14 +572,14 @@ internal class WalletRepository( } override fun deleteCurrentWallet() { - scope.launch { + appScope.launch(dispatcher) { val currentWallet = currentWallet.firstOrNull() ?: return@launch deleteWallet(currentWallet.id) } } override fun deleteWallet(id: String) { - scope.launch { + appScope.launch(dispatcher) { // get it before remove val currentWallet = currentWallet.firstOrNull() @@ -601,7 +597,7 @@ internal class WalletRepository( } override fun renameWallet(value: String, id: String) { - scope.launch { + appScope.launch(dispatcher) { database.walletDao().getById(id)?.wallet?.copy(name = value)?.let { database.walletDao().add(listOf(it)) } @@ -609,7 +605,7 @@ internal class WalletRepository( } override fun renameCurrentWallet(value: String) { - scope.launch { + appScope.launch(dispatcher) { currentWallet.firstOrNull()?.let { wallet -> database.walletDao().getById(wallet.id)?.wallet }?.copy(name = value)?.let { @@ -634,7 +630,7 @@ internal class WalletRepository( onDone: (String?) -> Unit, onError: (Throwable) -> Unit ) { - scope.launch { + appScope.launch(dispatcher) { currentWallet.firstOrNull()?.let { wallet -> val data = when (collectible.contract.schema) { CollectibleContractSchema.ERC721 -> listOf( @@ -685,7 +681,7 @@ internal class WalletRepository( onDone: (String?) -> Unit, onError: (Throwable) -> Unit, ) { - scope.launch { + appScope.launch(dispatcher) { val wallet = currentWallet.filterNotNull().first() if (wallet.fromWalletConnect) { walletConnectManager.sendToken( @@ -743,7 +739,7 @@ internal class WalletRepository( onDone: (String?) -> Unit, onError: (Throwable) -> Unit, ) { - scope.launch { + appScope.launch(dispatcher) { val isNativeToken = tokenData.address == database.chainDao().getByIdFlow(tokenData.chainType.chainId) .firstOrNull()?.token?.address @@ -801,7 +797,7 @@ internal class WalletRepository( } override suspend fun getEnsAddress(chainType: ChainType, name: String): String { - return withContext(Dispatchers.IO) { + return withContext(dispatcher) { val web3 = Web3j.build(chainType.httpService) EnsResolver(web3).resolve(name).apply { web3.shutdown() @@ -826,7 +822,7 @@ internal class WalletRepository( } override suspend fun refreshWallet() { - withContext(tokenScope.coroutineContext) { + withContext(dispatcher) { refreshCurrentWalletToken() refreshCurrentWalletCollectibles() refreshNativeTokens() diff --git a/wallet/src/androidMain/kotlin/com/dimension/maskbook/wallet/route/WalletsRoute.kt b/wallet/src/androidMain/kotlin/com/dimension/maskbook/wallet/route/WalletsRoute.kt index c34c1ee9..995b460c 100644 --- a/wallet/src/androidMain/kotlin/com/dimension/maskbook/wallet/route/WalletsRoute.kt +++ b/wallet/src/androidMain/kotlin/com/dimension/maskbook/wallet/route/WalletsRoute.kt @@ -30,6 +30,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext @@ -97,6 +98,7 @@ import com.dimension.maskbook.wallet.viewmodel.wallets.management.WalletRenameVi import com.dimension.maskbook.wallet.viewmodel.wallets.management.WalletSwitchEditViewModel import com.dimension.maskbook.wallet.viewmodel.wallets.management.WalletSwitchViewModel import com.dimension.maskbook.wallet.viewmodel.wallets.management.WalletTransactionHistoryViewModel +import kotlinx.coroutines.launch import org.koin.androidx.compose.get import org.koin.androidx.compose.getViewModel import org.koin.core.parameter.parametersOf @@ -568,8 +570,11 @@ fun WalletIntroHostLegal( val password by repo.paymentPassword.observeAsState(initial = null) val enableBiometric by repo.biometricEnabled.observeAsState(initial = false) val shouldShowLegalScene by repo.shouldShowLegalScene.observeAsState(initial = true) + val biometricEnableViewModel: BiometricEnableViewModel = getViewModel() val context = LocalContext.current + val scope = rememberCoroutineScope() + val next: () -> Unit = { val route = if (password.isNullOrEmpty()) { WalletRoute.WalletIntroHostPassword(type.name) @@ -594,7 +599,9 @@ fun WalletIntroHostLegal( LegalScene( onBack = onBack, onAccept = { - repo.setShouldShowLegalScene(false) + scope.launch { + repo.setShouldShowLegalScene(false) + } }, onBrowseAgreement = { context.startActivity(