1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-08 05:47:05 +09:00

DROID-2593 Auth | Tech | Debug goroutines stack (#1301)

This commit is contained in:
Konstantin Ivanov 2024-06-21 10:52:14 +02:00 committed by GitHub
parent 8d9beec534
commit d6d871fe57
Signed by: github
GPG key ID: B5690EEEBB952194
8 changed files with 215 additions and 49 deletions

View file

@ -1,10 +1,13 @@
package com.anytypeio.anytype.di.feature.onboarding
import android.content.Context
import androidx.lifecycle.ViewModelProvider
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.core_utils.di.scope.PerScreen
import com.anytypeio.anytype.di.common.ComponentDependencies
import com.anytypeio.anytype.presentation.onboarding.OnboardingViewModel
import com.anytypeio.anytype.presentation.util.downloader.UriFileProvider
import com.anytypeio.anytype.providers.DefaultUriFileProvider
import com.anytypeio.anytype.ui.onboarding.OnboardingFragment
import dagger.Binds
import dagger.Component
@ -34,9 +37,16 @@ object OnboardingModule {
@Binds
@AuthScreenScope
fun bindViewModelFactory(factory: OnboardingViewModel.Factory): ViewModelProvider.Factory
@Binds
@PerScreen
fun bindUriFileProvider(
defaultProvider: DefaultUriFileProvider
): UriFileProvider
}
}
interface OnboardingDependencies : ComponentDependencies {
fun analytics(): Analytics
fun context(): Context
}

View file

@ -1,5 +1,6 @@
package com.anytypeio.anytype.di.feature.onboarding.login
import android.content.Context
import androidx.lifecycle.ViewModelProvider
import com.anytypeio.anytype.CrashReporter
import com.anytypeio.anytype.analytics.base.Analytics
@ -7,7 +8,10 @@ import com.anytypeio.anytype.core_utils.di.scope.PerScreen
import com.anytypeio.anytype.di.common.ComponentDependencies
import com.anytypeio.anytype.domain.account.AwaitAccountStartManager
import com.anytypeio.anytype.domain.auth.repo.AuthRepository
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.block.repo.BlockRepository
import com.anytypeio.anytype.domain.config.ConfigStorage
import com.anytypeio.anytype.domain.debugging.DebugGoroutines
import com.anytypeio.anytype.domain.device.PathProvider
import com.anytypeio.anytype.domain.misc.LocaleProvider
import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider
@ -16,9 +20,12 @@ import com.anytypeio.anytype.domain.search.ObjectTypesSubscriptionManager
import com.anytypeio.anytype.domain.search.RelationsSubscriptionManager
import com.anytypeio.anytype.domain.spaces.SpaceDeletedStatusWatcher
import com.anytypeio.anytype.presentation.onboarding.login.OnboardingMnemonicLoginViewModel
import com.anytypeio.anytype.presentation.util.downloader.UriFileProvider
import com.anytypeio.anytype.providers.DefaultUriFileProvider
import dagger.Binds
import dagger.Component
import dagger.Module
import dagger.Provides
@Component(
dependencies = [OnboardingMnemonicLoginDependencies::class],
@ -41,8 +48,29 @@ interface OnboardingMnemonicLoginComponent {
@Module
object OnboardingMnemonicLoginModule {
@JvmStatic
@Provides
@PerScreen
fun bindDebugGoroutines(
repo: BlockRepository,
dispatchers: AppCoroutineDispatchers,
context: Context
): DebugGoroutines = DebugGoroutines(
repo = repo,
dispatchers = dispatchers,
cacheDir = context.cacheDir.path
)
@Module
interface Declarations {
@Binds
@PerScreen
fun bindUriFileProvider(
defaultProvider: DefaultUriFileProvider
): UriFileProvider
@Binds
@PerScreen
fun bindViewModelFactory(
@ -64,4 +92,7 @@ interface OnboardingMnemonicLoginDependencies : ComponentDependencies {
fun localeProvider(): LocaleProvider
fun awaitAccountStartManager(): AwaitAccountStartManager
fun userPermissionProvider() : UserPermissionProvider
fun provideAppCoroutineDispatchers(): AppCoroutineDispatchers
fun provideBlockRepository(): BlockRepository
fun provideContext(): Context
}

View file

@ -71,6 +71,7 @@ import com.anytypeio.anytype.core_ui.MNEMONIC_WORD_COUNT
import com.anytypeio.anytype.core_ui.MnemonicPhrasePaletteColors
import com.anytypeio.anytype.core_ui.views.BaseAlertDialog
import com.anytypeio.anytype.core_utils.ext.argOrNull
import com.anytypeio.anytype.core_utils.ext.shareFirstFileFromPath
import com.anytypeio.anytype.core_utils.ext.toast
import com.anytypeio.anytype.core_utils.insets.RootViewDeferringInsetsCallback
import com.anytypeio.anytype.di.common.componentManager
@ -80,6 +81,7 @@ import com.anytypeio.anytype.presentation.onboarding.OnboardingStartViewModel.Si
import com.anytypeio.anytype.presentation.onboarding.OnboardingViewModel
import com.anytypeio.anytype.presentation.onboarding.login.OnboardingMnemonicLoginViewModel
import com.anytypeio.anytype.presentation.onboarding.signup.OnboardingSetProfileNameViewModel
import com.anytypeio.anytype.presentation.util.downloader.UriFileProvider
import com.anytypeio.anytype.ui.home.HomeScreenFragment
import com.anytypeio.anytype.ui.onboarding.screens.AuthScreenWrapper
import com.anytypeio.anytype.ui.onboarding.screens.signin.RecoveryScreenWrapper
@ -396,12 +398,13 @@ class OnboardingFragment : Fragment() {
}
}
LaunchedEffect(Unit) {
vm.navigation.collect { navigation ->
when (navigation) {
OnboardingMnemonicLoginViewModel.Navigation.Exit -> {
vm.command.collect { command ->
Timber.d("Command: $command")
when (command) {
OnboardingMnemonicLoginViewModel.Command.Exit -> {
navController.popBackStack()
}
OnboardingMnemonicLoginViewModel.Navigation.NavigateToHomeScreen -> {
OnboardingMnemonicLoginViewModel.Command.NavigateToHomeScreen -> {
runCatching {
findNavController().navigate(
R.id.action_openHome,
@ -411,7 +414,7 @@ class OnboardingFragment : Fragment() {
Timber.e(it, "Error while trying to open home screen from onboarding")
}
}
OnboardingMnemonicLoginViewModel.Navigation.NavigateToMigrationErrorScreen -> {
OnboardingMnemonicLoginViewModel.Command.NavigateToMigrationErrorScreen -> {
runCatching {
findNavController().navigate(
R.id.migrationNeededScreen,
@ -426,6 +429,18 @@ class OnboardingFragment : Fragment() {
Timber.e(it, "Error while trying to open migration screen from onboarding")
}
}
is OnboardingMnemonicLoginViewModel.Command.ShareDebugGoroutines -> {
try {
this@OnboardingFragment.shareFirstFileFromPath(command.path, command.uriFileProvider)
} catch (e: Exception) {
Timber.e(e, "Error while stack goroutines debug").also {
toast("Error while stack goroutines debug. Please try again later.")
}
}
}
is OnboardingMnemonicLoginViewModel.Command.ShowToast -> {
toast(command.message)
}
}
}
}

View file

@ -1,8 +1,11 @@
package com.anytypeio.anytype.ui.onboarding.screens.signin
import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@ -21,6 +24,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
@ -39,6 +43,7 @@ import com.anytypeio.anytype.core_ui.ColorButtonRegular
import com.anytypeio.anytype.core_ui.MnemonicPhrasePaletteColors
import com.anytypeio.anytype.core_ui.OnBoardingTextPrimaryColor
import com.anytypeio.anytype.core_ui.foundation.noRippleClickable
import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable
import com.anytypeio.anytype.core_ui.views.ButtonSize
import com.anytypeio.anytype.core_ui.views.ConditionLogin
import com.anytypeio.anytype.core_ui.views.OnBoardingButtonPrimary
@ -48,6 +53,7 @@ import com.anytypeio.anytype.core_utils.ext.toast
import com.anytypeio.anytype.presentation.onboarding.login.OnboardingMnemonicLoginViewModel
import com.anytypeio.anytype.presentation.onboarding.login.OnboardingMnemonicLoginViewModel.SetupState
import com.anytypeio.anytype.ui.onboarding.OnboardingMnemonicInput
import timber.log.Timber
@Composable
fun RecoveryScreenWrapper(
@ -60,7 +66,8 @@ fun RecoveryScreenWrapper(
onNextClicked = vm::onLoginClicked,
onActionDoneClicked = vm::onActionDone,
onScanQrClicked = onScanQrClick,
isLoading = vm.state.collectAsState().value is SetupState.InProgress
isLoading = vm.state.collectAsState().value is SetupState.InProgress,
onEnterMyVaultClicked = vm::onEnterMyVaultClicked
)
}
@ -70,32 +77,42 @@ fun RecoveryScreen(
onNextClicked: (Mnemonic) -> Unit,
onActionDoneClicked: (Mnemonic) -> Unit,
onScanQrClicked: () -> Unit,
isLoading: Boolean
isLoading: Boolean,
onEnterMyVaultClicked: () -> Unit
) {
val focus = LocalFocusManager.current
val context = LocalContext.current
val text = remember { mutableStateOf("") }
Box(
modifier = Modifier.fillMaxSize()
) {
Image(
modifier = Modifier
.padding(top = 12.dp, start = 9.dp)
.noRippleClickable {
focus.clearFocus()
onBackClicked()
},
painter = painterResource(id = R.drawable.ic_back_onboarding_32),
contentDescription = "Back button"
)
Text(
modifier = Modifier
.noRippleClickable{ onEnterMyVaultClicked() }
.align(Alignment.TopCenter)
.padding(top = 21.dp)
.padding(top = 17.dp, start = 18.dp, end = 18.dp)
,
text = stringResource(id = R.string.onboarding_enter_my_vault),
style = TitleLogin.copy(
color = OnBoardingTextPrimaryColor
color = colorResource(id = R.color.text_white)
)
)
val text = remember {
mutableStateOf("")
}
val focus = LocalFocusManager.current
val context = LocalContext.current
val emptyRecoveryPhraseError = stringResource(R.string.onboarding_your_key_can_t_be_empty)
LazyColumn(
modifier = Modifier.padding(top = 71.dp),
content = {
item {
OnboardingMnemonicInput(
@ -103,7 +120,6 @@ fun RecoveryScreen(
.padding(
start = 18.dp,
end = 18.dp,
top = 71.dp,
bottom = 18.dp
)
.height(165.dp)
@ -182,17 +198,6 @@ fun RecoveryScreen(
}
}
)
Image(
modifier = Modifier
.align(Alignment.TopStart)
.padding(top = 16.dp, start = 9.dp)
.noRippleClickable {
focus.clearFocus()
onBackClicked()
},
painter = painterResource(id = R.drawable.ic_back_onboarding_32),
contentDescription = "Back button"
)
}
}
@ -233,7 +238,8 @@ object MnemonicPhraseFormatter : VisualTransformation {
}
}
@Preview
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Light Mode")
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Dark Mode")
@Composable
fun RecoveryScreenPreview() {
RecoveryScreen(
@ -241,11 +247,13 @@ fun RecoveryScreenPreview() {
onNextClicked = {},
onActionDoneClicked = {},
onScanQrClicked = {},
isLoading = false
isLoading = false,
onEnterMyVaultClicked = {}
)
}
@Preview
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Light Mode")
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Dark Mode")
@Composable
fun RecoveryScreenLoadingPreview() {
RecoveryScreen(
@ -253,6 +261,7 @@ fun RecoveryScreenLoadingPreview() {
onNextClicked = {},
onActionDoneClicked = {},
onScanQrClicked = {},
isLoading = true
isLoading = true,
onEnterMyVaultClicked = {}
)
}

View file

@ -46,8 +46,10 @@ import com.anytypeio.anytype.core_utils.const.FileConstants.REQUEST_MEDIA_CODE
import com.anytypeio.anytype.core_utils.const.MimeTypes.MIME_EXTRA_IMAGE_VIDEO
import com.anytypeio.anytype.core_utils.const.MimeTypes.MIME_EXTRA_YAML
import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetComposeFragment
import com.anytypeio.anytype.presentation.util.downloader.UriFileProvider
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import timber.log.Timber
@ -370,6 +372,27 @@ fun NavController.safeNavigate(
}
}
fun Fragment.shareFirstFileFromPath(path: String, uriFileProvider: UriFileProvider) {
try {
val dirPath = File(path)
if (dirPath.exists() && dirPath.isDirectory) {
val files = dirPath.listFiles()
val firstFile = files?.firstOrNull { it != null && it.exists() && it.isFile }
if (firstFile != null) {
val uri = uriFileProvider.getUriForFile(firstFile)
shareFile(uri)
} else {
toast("No valid files to share in the directory.")
}
} else {
toast("Directory does not exist or is not a directory.")
}
} catch (e: Exception) {
Timber.e(e, "Error while sharing file")
toast("Could not share file: ${e.message}")
}
}
fun Fragment.shareFile(uri: Uri) {
try {
val shareIntent: Intent = Intent().apply {

View file

@ -3,16 +3,39 @@ package com.anytypeio.anytype.domain.debugging
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.base.ResultInteractor
import com.anytypeio.anytype.domain.block.repo.BlockRepository
import java.io.File
import javax.inject.Inject
class DebugGoroutines @Inject constructor(
private val cacheDir: String,
private val repo: BlockRepository,
dispatchers: AppCoroutineDispatchers
) : ResultInteractor<DebugGoroutines.Params, Unit>(dispatchers.io) {
) : ResultInteractor<DebugGoroutines.Params, String>(dispatchers.io) {
override suspend fun doWork(params: Params) {
repo.debugStackGoroutines(params.path)
override suspend fun doWork(params: Params): String {
val path: String
val resultFilePath: String
if (params.path == null) {
// Construct the path using the current time to ensure uniqueness
path = "${cacheDir}/debug/goroutines/${System.currentTimeMillis()}/"
// Middleware put the log in a subdirectory called logs
resultFilePath = "$path/logs/"
// Create the directories if they do not exist
File(path).apply { mkdirs() }
} else {
path = params.path
resultFilePath = path
}
// Perform the debug operation
repo.debugStackGoroutines(path)
// Return the result file path
return resultFilePath
}
data class Params(val path: String)
// Data class for the parameters, with a default value of null for the path
data class Params(val path: String? = null)
}

View file

@ -616,7 +616,7 @@
<string name="sign_up">Sign up</string>
<string name="type_your_recovery_phrase">or type your Key</string>
<string name="login_with_recovery_phrase">Login with your Key</string>
<string name="or_scan_qr_code">Scan QR code</string>
<string name="or_scan_qr_code">Scan Qr code</string>
<string name="choose_pin_code">Choose pin code</string>
<string name="congratulations">Congratulations!</string>
<string name="time_to_update_title">It\'s time to update</string>
@ -1102,7 +1102,7 @@
<string name="onboarding_soul_creation_description">Only seen by people you share something with. There is no central registry of these names.</string>
<string name="onboarding_soul_creation_placeholder">Anytype Identity</string>
<string name="onboarding_soul_creation">Creating your Identity…</string>
<string name="onboarding_type_your_key">Type your Key</string>
<string name="onboarding_type_your_key">Type your recovery phrase</string>
<string name="onboarding_login_or">OR</string>
<string name="onboarding_entering_void_title">Entering the Void</string>
<string name="onboarding_your_key_can_t_be_empty">Your Key can\'t be empty</string>

View file

@ -18,7 +18,9 @@ import com.anytypeio.anytype.domain.auth.interactor.RecoverWallet
import com.anytypeio.anytype.domain.auth.interactor.SaveMnemonic
import com.anytypeio.anytype.domain.auth.interactor.SelectAccount
import com.anytypeio.anytype.domain.auth.interactor.StartLoadingAccounts
import com.anytypeio.anytype.domain.base.fold
import com.anytypeio.anytype.domain.config.ConfigStorage
import com.anytypeio.anytype.domain.debugging.DebugGoroutines
import com.anytypeio.anytype.domain.device.PathProvider
import com.anytypeio.anytype.domain.misc.LocaleProvider
import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider
@ -28,6 +30,7 @@ import com.anytypeio.anytype.domain.spaces.SpaceDeletedStatusWatcher
import com.anytypeio.anytype.presentation.extension.proceedWithAccountEvent
import com.anytypeio.anytype.presentation.extension.sendAnalyticsOnboardingLoginEvent
import com.anytypeio.anytype.presentation.splash.SplashViewModel
import com.anytypeio.anytype.presentation.util.downloader.UriFileProvider
import javax.inject.Inject
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableSharedFlow
@ -51,23 +54,34 @@ class OnboardingMnemonicLoginViewModel @Inject constructor(
private val crashReporter: CrashReporter,
private val configStorage: ConfigStorage,
private val localeProvider: LocaleProvider,
private val userPermissionProvider: UserPermissionProvider
private val userPermissionProvider: UserPermissionProvider,
private val debugGoroutines: DebugGoroutines,
private val uriFileProvider: UriFileProvider
) : ViewModel() {
private val jobs = mutableListOf<Job>()
private var goroutinesJob : Job? = null
val sideEffects = MutableSharedFlow<SideEffect>()
val state = MutableStateFlow<SetupState>(SetupState.Idle)
val navigation = MutableSharedFlow<Navigation>()
val command = MutableSharedFlow<Command>(replay = 0)
val error by lazy { MutableStateFlow(NO_ERROR) }
private var debugClickCount = 0
private val _fiveClicks = MutableStateFlow(false)
init {
viewModelScope.sendEvent(
analytics = analytics,
eventName = EventsDictionary.loginScreenShow
)
viewModelScope.launch {
_fiveClicks.collect {
if (it) proceedWithGoroutinesDebug()
}
}
}
fun onLoginClicked(chain: String) {
@ -177,7 +191,7 @@ class OnboardingMnemonicLoginViewModel @Inject constructor(
}
Timber.e(e, "Error while account loading")
// TODO refact
viewModelScope.launch { navigation.emit(Navigation.Exit) }
viewModelScope.launch { command.emit(Command.Exit) }
},
fnR = {
Timber.d("Account loading successfully finished")
@ -248,7 +262,7 @@ class OnboardingMnemonicLoginViewModel @Inject constructor(
private fun navigateToMigrationErrorScreen() {
viewModelScope.launch {
navigation.emit(Navigation.NavigateToMigrationErrorScreen)
command.emit(Command.NavigateToMigrationErrorScreen)
}
}
@ -261,7 +275,7 @@ class OnboardingMnemonicLoginViewModel @Inject constructor(
private fun navigateToDashboard() {
viewModelScope.launch {
navigation.emit(Navigation.NavigateToHomeScreen)
command.emit(Command.NavigateToHomeScreen)
}
}
@ -270,6 +284,41 @@ class OnboardingMnemonicLoginViewModel @Inject constructor(
proceedWithSelectingAccount(id)
}
fun onEnterMyVaultClicked() {
Timber.d("onEnterMyVaultClicked")
viewModelScope.launch {
debugClickCount++
if (debugClickCount == 5) {
_fiveClicks.emit(true)
debugClickCount = 0
} else {
_fiveClicks.emit(false)
}
}
}
private fun proceedWithGoroutinesDebug() {
if (goroutinesJob?.isActive == true) {
return
}
Timber.d("proceedWithGoroutinesDebug")
goroutinesJob = viewModelScope.launch {
debugGoroutines.async(DebugGoroutines.Params()).fold(
onSuccess = { path ->
command.emit(Command.ShareDebugGoroutines(path, uriFileProvider))
},
onFailure = {
Timber.e(it, "Error while collecting goroutines diagnostics")
}
)
}
}
override fun onCleared() {
super.onCleared()
goroutinesJob?.cancel()
}
sealed class SideEffect {
sealed class Error : SideEffect() {
data object InvalidMnemonic : Error()
@ -285,10 +334,12 @@ class OnboardingMnemonicLoginViewModel @Inject constructor(
object Failed: SetupState()
}
sealed class Navigation {
object Exit : Navigation()
object NavigateToMigrationErrorScreen : Navigation()
object NavigateToHomeScreen: Navigation()
sealed class Command {
data object Exit : Command()
data object NavigateToMigrationErrorScreen : Command()
data object NavigateToHomeScreen: Command()
data class ShowToast(val message: String) : Command()
data class ShareDebugGoroutines(val path: String, val uriFileProvider: UriFileProvider) : Command()
}
class Factory @Inject constructor(
@ -306,7 +357,9 @@ class OnboardingMnemonicLoginViewModel @Inject constructor(
private val crashReporter: CrashReporter,
private val configStorage: ConfigStorage,
private val localeProvider: LocaleProvider,
private val userPermissionProvider: UserPermissionProvider
private val userPermissionProvider: UserPermissionProvider,
private val debugGoroutines: DebugGoroutines,
private val uriFileProvider: UriFileProvider
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
@ -325,7 +378,9 @@ class OnboardingMnemonicLoginViewModel @Inject constructor(
spaceDeletedStatusWatcher = spaceDeletedStatusWatcher,
selectAccount = selectAccount,
localeProvider = localeProvider,
userPermissionProvider = userPermissionProvider
userPermissionProvider = userPermissionProvider,
debugGoroutines = debugGoroutines,
uriFileProvider = uriFileProvider
) as T
}
}