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

DROID-449 Research settings of app to add debug (#2725)

DROID-449 App | Tech | Add debug screen
This commit is contained in:
Mikhail 2022-11-30 15:44:40 +03:00 committed by GitHub
parent 258c05183f
commit 548f3b0039
Signed by: github
GPG key ID: 4AEE18F83AFDEB23
53 changed files with 838 additions and 108 deletions

View file

@ -176,6 +176,7 @@ dependencies {
implementation applicationDependencies.composeFoundation
implementation applicationDependencies.composeMaterial
implementation applicationDependencies.composeToolingPreview
implementation applicationDependencies.preference
debugImplementation applicationDependencies.composeTooling
implementation databaseDependencies.room

View file

@ -23,9 +23,9 @@
android:theme="@style/AppTheme">
<activity
android:name="com.anytypeio.anytype.ui.main.MainActivity"
android:screenOrientation="portrait"
android:exported="true"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize">
<intent-filter android:label="filter">
<action android:name="android.intent.action.VIEW" />
@ -43,11 +43,23 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ui.settings.system.SettingsActivity"
android:exported="true"
android:launchMode="singleInstance"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.APPLICATION_PREFERENCES" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:screenOrientation="fullSensor"
android:exported="false"
android:screenOrientation="fullSensor"
tools:replace="screenOrientation" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"

View file

@ -1,24 +1,32 @@
package com.anytypeio.anytype.app
import android.content.Context
import android.content.SharedPreferences
import com.anytypeio.anytype.BuildConfig
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_utils.tools.FeatureToggles
import javax.inject.Inject
class DefaultFeatureToggles @Inject constructor() : FeatureToggles {
class DefaultFeatureToggles @Inject constructor(
private val context: Context,
@TogglePrefs private val prefs: SharedPreferences
) : FeatureToggles {
override val isLogFromMiddlewareLibrary =
BuildConfig.LOG_FROM_MW_LIBRARY && BuildConfig.DEBUG
BuildConfig.LOG_FROM_MW_LIBRARY && isDebug
override val isLogMiddlewareInteraction =
BuildConfig.LOG_MW_INTERACTION && BuildConfig.DEBUG
BuildConfig.LOG_MW_INTERACTION && isDebug
override val isLogDashboardReducer =
BuildConfig.LOG_DASHBOARD_REDUCER && BuildConfig.DEBUG
BuildConfig.LOG_DASHBOARD_REDUCER && isDebug
override val isLogEditorViewModelEvents =
BuildConfig.LOG_EDITOR_VIEWMODEL_EVENTS && BuildConfig.DEBUG
BuildConfig.LOG_EDITOR_VIEWMODEL_EVENTS && isDebug
override val isLogEditorControlPanelMachine =
BuildConfig.LOG_EDITOR_CONTROL_PANEL && BuildConfig.DEBUG
BuildConfig.LOG_EDITOR_CONTROL_PANEL && isDebug
override val isDebug: Boolean
get() = prefs.getBoolean(context.getString(R.string.debug_mode), BuildConfig.DEBUG)
}

View file

@ -0,0 +1,8 @@
package com.anytypeio.anytype.app
import javax.inject.Qualifier
@Qualifier
@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME)
annotation class TogglePrefs

View file

@ -1,5 +1,6 @@
package com.anytypeio.anytype.di.feature.settings
import android.content.Context
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.core_utils.di.scope.PerScreen
import com.anytypeio.anytype.domain.account.DeleteAccount
@ -7,9 +8,15 @@ import com.anytypeio.anytype.domain.auth.interactor.Logout
import com.anytypeio.anytype.domain.auth.repo.AuthRepository
import com.anytypeio.anytype.domain.block.repo.BlockRepository
import com.anytypeio.anytype.domain.config.ConfigStorage
import com.anytypeio.anytype.domain.debugging.DebugSync
import com.anytypeio.anytype.domain.device.ClearFileCache
import com.anytypeio.anytype.presentation.util.downloader.UriFileProvider
import com.anytypeio.anytype.providers.DefaultUriFileProvider
import com.anytypeio.anytype.ui.settings.AccountAndDataFragment
import com.anytypeio.anytype.ui_settings.account.AccountAndDataViewModel
import com.anytypeio.anytype.ui_settings.account.repo.DebugSyncShareDownloader
import com.anytypeio.anytype.ui_settings.account.repo.FileSaver
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.Subcomponent
@ -27,21 +34,42 @@ interface AccountAndDataSubComponent {
fun inject(fragment: AccountAndDataFragment)
}
@Module
@Module(includes = [AccountAndDataModule.Bindings::class])
object AccountAndDataModule {
@JvmStatic
@Provides
@PerScreen
fun provideViewModelFactory(
clearFileCache: ClearFileCache,
deleteAccount: DeleteAccount,
debugSyncShareDownloader: DebugSyncShareDownloader,
analytics: Analytics
): AccountAndDataViewModel.Factory = AccountAndDataViewModel.Factory(
clearFileCache = clearFileCache,
deleteAccount = deleteAccount,
debugSyncShareDownloader = debugSyncShareDownloader,
analytics = analytics
)
@Provides
@PerScreen
fun provideDebugSync(repo: BlockRepository): DebugSync = DebugSync(repo = repo)
@Provides
@PerScreen
fun provideFileSaver(
uriFileProvider: UriFileProvider,
context: Context
): FileSaver = FileSaver(context, uriFileProvider)
@Provides
@PerScreen
fun providesDebugSyncShareDownloader(
debugSync: DebugSync,
fileSaver: FileSaver
): DebugSyncShareDownloader = DebugSyncShareDownloader(debugSync, fileSaver)
@JvmStatic
@Provides
@PerScreen
@ -62,4 +90,14 @@ object AccountAndDataModule {
@Provides
@PerScreen
fun deleteAccount(repo: AuthRepository): DeleteAccount = DeleteAccount(repo)
@Module
interface Bindings {
@PerScreen
@Binds
fun bindUriFileProvider(
defaultProvider: DefaultUriFileProvider
): UriFileProvider
}
}

View file

@ -226,8 +226,9 @@ object DataModule {
fun provideMiddleware(
service: MiddlewareService,
factory: MiddlewareFactory,
logger: MiddlewareProtobufLogger
): Middleware = Middleware(service, factory, logger)
logger: MiddlewareProtobufLogger,
protobufConverter: ProtobufConverterProvider
): Middleware = Middleware(service, factory, logger, protobufConverter)
@JvmStatic
@Provides

View file

@ -1,5 +1,9 @@
package com.anytypeio.anytype.di.main
import android.content.Context
import android.content.SharedPreferences
import androidx.preference.PreferenceManager
import com.anytypeio.anytype.app.TogglePrefs
import com.anytypeio.anytype.app.DefaultFeatureToggles
import com.anytypeio.anytype.core_utils.tools.DefaultUrlValidator
import com.anytypeio.anytype.core_utils.tools.FeatureToggles
@ -21,6 +25,13 @@ object UtilModule {
@Singleton
fun provideUrlBuilder(gateway: Gateway): UrlBuilder = UrlBuilder(gateway)
@JvmStatic
@Provides
@Singleton
@TogglePrefs
fun providesSharedPreferences(
context: Context
): SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
@Module
interface Bindings {

View file

@ -31,7 +31,6 @@ import com.anytypeio.anytype.presentation.navigation.AppNavigation
import com.anytypeio.anytype.presentation.wallpaper.WallpaperColor
import com.anytypeio.anytype.ui.editor.CreateObjectFragment
import com.anytypeio.anytype.ui_settings.appearance.ThemeApplicator
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import timber.log.Timber

View file

@ -1,5 +1,8 @@
package com.anytypeio.anytype.ui.settings
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@ -10,11 +13,13 @@ import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.core.os.bundleOf
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.anytypeio.anytype.BuildConfig
import com.anytypeio.anytype.R
import com.anytypeio.anytype.analytics.base.EventsDictionary
import com.anytypeio.anytype.core_utils.ext.subscribe
import com.anytypeio.anytype.core_utils.ext.toast
import com.anytypeio.anytype.core_utils.tools.FeatureToggles
import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetComposeFragment
import com.anytypeio.anytype.di.common.componentManager
import com.anytypeio.anytype.ui.auth.account.DeleteAccountWarning
@ -22,6 +27,7 @@ import com.anytypeio.anytype.ui.dashboard.ClearCacheAlertFragment
import com.anytypeio.anytype.ui.profile.KeychainPhraseDialog
import com.anytypeio.anytype.ui_settings.account.AccountAndDataScreen
import com.anytypeio.anytype.ui_settings.account.AccountAndDataViewModel
import timber.log.Timber
import javax.inject.Inject
class AccountAndDataFragment : BaseBottomSheetComposeFragment() {
@ -29,10 +35,14 @@ class AccountAndDataFragment : BaseBottomSheetComposeFragment() {
@Inject
lateinit var factory: AccountAndDataViewModel.Factory
@Inject
lateinit var toggles: FeatureToggles
private val vm by viewModels<AccountAndDataViewModel> { factory }
private val onKeychainPhraseClicked = {
val bundle = bundleOf(KeychainPhraseDialog.ARG_SCREEN_TYPE to EventsDictionary.Type.screenSettings)
val bundle =
bundleOf(KeychainPhraseDialog.ARG_SCREEN_TYPE to EventsDictionary.Type.screenSettings)
findNavController().navigate(R.id.keychainDialog, bundle)
}
@ -45,6 +55,11 @@ class AccountAndDataFragment : BaseBottomSheetComposeFragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
jobs += lifecycleScope.subscribe(vm.debugSyncReportUri) { uri ->
if (uri != null) {
shareFile(uri)
}
}
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
@ -54,14 +69,36 @@ class AccountAndDataFragment : BaseBottomSheetComposeFragment() {
onClearFileCachedClicked = { proceedWithClearFileCacheWarning() },
onDeleteAccountClicked = { proceedWithAccountDeletion() },
onLogoutClicked = onLogoutClicked,
onDebugSyncReportClicked = { vm.onDebugSyncReportClicked() },
isLogoutInProgress = vm.isLoggingOut.collectAsState().value,
isClearCacheInProgress = vm.isClearFileCacheInProgress.collectAsState().value
isClearCacheInProgress = vm.isClearFileCacheInProgress.collectAsState().value,
isDebugSyncReportInProgress = vm.isDebugSyncReportInProgress.collectAsState().value,
isShowDebug = toggles.isDebug
)
}
}
}
}
private fun shareFile(uri: Uri) {
try {
val shareIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
putExtra(Intent.EXTRA_STREAM, uri)
type = requireContext().contentResolver.getType(uri)
}
startActivity(shareIntent)
} catch (e: Exception) {
if (e is ActivityNotFoundException) {
toast("No application found to open the selected file")
} else {
toast("Could not open file: ${e.message}")
}
Timber.e(e, "Error while opening file")
}
}
private fun proceedWithClearFileCacheWarning() {
vm.onClearCacheButtonClicked()
val dialog = ClearCacheAlertFragment.new()

View file

@ -17,6 +17,7 @@ import com.anytypeio.anytype.core_utils.ext.visible
import com.anytypeio.anytype.core_utils.ui.BaseFragment
import com.anytypeio.anytype.databinding.FragmentDebugSettingsBinding
import com.anytypeio.anytype.di.common.componentManager
import com.anytypeio.anytype.domain.base.fold
import com.anytypeio.anytype.domain.config.GetDebugSettings
import com.anytypeio.anytype.domain.config.UseCustomContextMenu
import com.anytypeio.anytype.domain.debugging.DebugLocalStore
@ -54,9 +55,8 @@ class DebugSettingsFragment : BaseFragment<FragmentDebugSettingsBinding>(R.layou
binding.btnSync.setOnClickListener {
viewLifecycleOwner.lifecycleScope.launch {
debugSync.invoke(Unit).proceed(
failure = {},
success = { status -> saveToFile(status) }
debugSync.execute(Unit).fold(
onSuccess = { status -> saveToFile(status) }
)
}
}

View file

@ -0,0 +1,12 @@
package com.anytypeio.anytype.ui.settings.system
import android.os.Bundle
import androidx.preference.PreferenceFragmentCompat
import com.anytypeio.anytype.R
class PreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.preferences, rootKey)
}
}

View file

@ -0,0 +1,6 @@
package com.anytypeio.anytype.ui.settings.system
import androidx.appcompat.app.AppCompatActivity
import com.anytypeio.anytype.R
class SettingsActivity : AppCompatActivity(R.layout.activity_settings)

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/dashboard_background"
android:orientation="vertical">
<TextView
style="@style/DashboardGreetingTextStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="20dp"
android:text="@string/settings_screen"
android:textColor="@color/text_primary" />
<androidx.fragment.app.FragmentContainerView xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/fragment"
android:name="com.anytypeio.anytype.ui.settings.system.PreferenceFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:context=".ui.settings.system.SettingsActivity" />
</LinearLayout>

View file

@ -291,6 +291,8 @@ Do the computation of an expensive paragraph of text on a background thread:
<item quantity="one">%d object selected</item>
<item quantity="other">%d objects selected</item>
</plurals>
<string name="debug_mode">debug_mode</string>
<string name="settings_screen">Settings</string>
<string name="snack_link_to">linked to</string>
<string name="recovery_phrase_copied">Recovery phrase copied</string>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<SwitchPreference
android:defaultValue="false"
android:key="@string/debug_mode"
android:title="Debug mode" />
</PreferenceScreen>

View file

@ -12,4 +12,6 @@ interface FeatureToggles {
val isLogEditorControlPanelMachine: Boolean
val isDebug: Boolean
}

View file

@ -4,9 +4,20 @@ import android.os.Bundle
import android.view.View
import com.anytypeio.anytype.core_utils.R
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kotlinx.coroutines.Job
abstract class BaseBottomSheetComposeFragment : BottomSheetDialogFragment() {
protected val jobs = mutableListOf<Job>()
override fun onStop() {
super.onStop()
jobs.apply {
forEach { it.cancel() }
clear()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
injectDependencies()

View file

@ -459,6 +459,10 @@ class BlockDataRepository(
}
override suspend fun debugSync(): String = remote.debugSync()
override suspend fun debugTree(objectId: Id, path: String): String
= remote.debugTree(objectId = objectId, path = path)
override suspend fun debugLocalStore(path: String): String =
remote.debugLocalStore(path)

View file

@ -171,6 +171,9 @@ interface BlockDataStore {
suspend fun deleteRelationFromObject(ctx: Id, relation: Key): Payload
suspend fun debugSync(): String
suspend fun debugTree(objectId: Id, path: String): String
suspend fun debugLocalStore(path: String): String
suspend fun turnInto(

View file

@ -178,6 +178,9 @@ interface BlockRemote {
suspend fun deleteRelationFromObject(ctx: Id, relation: Key): Payload
suspend fun debugSync(): String
suspend fun debugTree(objectId: Id, path: String): String
suspend fun debugLocalStore(path: String): String
suspend fun updateDetail(

View file

@ -383,6 +383,10 @@ class BlockRemoteDataStore(private val remote: BlockRemote) : BlockDataStore {
): Payload = remote.deleteRelationFromObject(ctx = ctx, relation = relation)
override suspend fun debugSync(): String = remote.debugSync()
override suspend fun debugTree(objectId: Id, path: String): String =
remote.debugTree(objectId = objectId, path = path)
override suspend fun debugLocalStore(path: String): String = remote.debugLocalStore(path)
override suspend fun turnInto(

View file

@ -10,6 +10,7 @@ ext {
androidx_core_testing_version = '2.1.0'
androidx_security_crypto_version = '1.0.0'
androidx_compose_version = '1.2.1'
androidx_preference_version = '1.2.0'
// Other Android framework dependencies
appcompat_version = '1.3.0'
@ -137,44 +138,45 @@ ext {
composeTooling: "androidx.compose.ui:ui-tooling:$androidx_compose_version",
composeToolingPreview: "androidx.compose.ui:ui-tooling-preview:$androidx_compose_version",
composeFoundation: "androidx.compose.foundation:foundation:$androidx_compose_version",
composeMaterial: "androidx.compose.material:material:$androidx_compose_version"
composeMaterial: "androidx.compose.material:material:$androidx_compose_version",
preference : "androidx.preference:preference:$androidx_preference_version"
]
unitTesting = [
kotlin: "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version",
kotlinTest: "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version",
robolectricLatest: "org.robolectric:robolectric:$robolectric_latest_version",
junit: "junit:junit:$junit_version",
mockitoKotlin: "org.mockito.kotlin:mockito-kotlin:$mockito_kotlin_version",
kluent: "org.amshove.kluent:kluent:$kluent_version",
archCoreTesting: "androidx.arch.core:core-testing:$androidx_core_testing_version",
liveDataTesting: "com.jraska.livedata:testing-ktx:$live_data_testing_version",
coroutineTesting: "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutine_testing_version",
assertj: "com.squareup.assertj:assertj-android:1.0.0",
androidXTestCore: "androidx.test:core:$androidx_test_core_version",
timberJUnit: "net.lachlanmckee:timber-junit-rule:$timber_junit",
turbine: "app.cash.turbine:turbine:$turbine_version"
kotlin : "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version",
kotlinTest : "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version",
robolectricLatest: "org.robolectric:robolectric:$robolectric_latest_version",
junit : "junit:junit:$junit_version",
mockitoKotlin : "org.mockito.kotlin:mockito-kotlin:$mockito_kotlin_version",
kluent : "org.amshove.kluent:kluent:$kluent_version",
archCoreTesting : "androidx.arch.core:core-testing:$androidx_core_testing_version",
liveDataTesting : "com.jraska.livedata:testing-ktx:$live_data_testing_version",
coroutineTesting : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutine_testing_version",
assertj : "com.squareup.assertj:assertj-android:1.0.0",
androidXTestCore : "androidx.test:core:$androidx_test_core_version",
timberJUnit : "net.lachlanmckee:timber-junit-rule:$timber_junit",
turbine : "app.cash.turbine:turbine:$turbine_version"
]
acceptanceTesting = [
androidJUnit: "androidx.test.ext:junit:$android_junit_version",
testRunner: "androidx.test:runner:$runner_version",
testRules: "androidx.test:rules:$runner_version",
espressoCore: "androidx.test.espresso:espresso-core:$espresso_version",
espressoContrib: "androidx.test.espresso:espresso-contrib:$espresso_version",
espressoIntents: "androidx.test.espresso:espresso-intents:$espresso_version",
androidJUnit : "androidx.test.ext:junit:$android_junit_version",
testRunner : "androidx.test:runner:$runner_version",
testRules : "androidx.test:rules:$runner_version",
espressoCore : "androidx.test.espresso:espresso-core:$espresso_version",
espressoContrib : "androidx.test.espresso:espresso-contrib:$espresso_version",
espressoIntents : "androidx.test.espresso:espresso-intents:$espresso_version",
androidAnnotations: "androidx.annotation:annotation:$appcompat_version",
mockitoKotlin: "org.mockito.kotlin:mockito-kotlin:$mockito_kotlin_version",
mockitoAndroid: "org.mockito:mockito-android:$mockito_android_version",
disableAnimation: "com.bartoszlipinski:disable-animations-rule:$disable_animation_version",
fragmentTesting: "androidx.fragment:fragment-testing:$fragment_version",
navigationTesting: "androidx.navigation:navigation-testing:$navigation_version"
mockitoKotlin : "org.mockito.kotlin:mockito-kotlin:$mockito_kotlin_version",
mockitoAndroid : "org.mockito:mockito-android:$mockito_android_version",
disableAnimation : "com.bartoszlipinski:disable-animations-rule:$disable_animation_version",
fragmentTesting : "androidx.fragment:fragment-testing:$fragment_version",
navigationTesting : "androidx.navigation:navigation-testing:$navigation_version"
]
development = [
leakCanary: "com.squareup.leakcanary:leakcanary-android:${leakCanaryVersion}",
leakCanaryNoop: "com.squareup.leakcanary:leakcanary-android-no-op:${leakCanaryVersion}",
stetho: "com.facebook.stetho:stetho:${stethoVersion}"
leakCanary : "com.squareup.leakcanary:leakcanary-android:${leakCanaryVersion}",
leakCanaryNoop: "com.squareup.leakcanary:leakcanary-android-no-op:${leakCanaryVersion}",
stetho : "com.facebook.stetho:stetho:${stethoVersion}"
]
protobuf = [
@ -185,18 +187,18 @@ ext {
]
db = [
room: "androidx.room:room-runtime:$room_version",
roomKtx: "androidx.room:room-ktx:$room_version",
annotations: "androidx.room:room-compiler:$room_version",
roomTesting: "androidx.room:room-testing:$room_version"
room : "androidx.room:room-runtime:$room_version",
roomKtx : "androidx.room:room-ktx:$room_version",
annotations: "androidx.room:room-compiler:$room_version",
roomTesting: "androidx.room:room-testing:$room_version"
]
analytics = [
amplitude: "com.amplitude:android-sdk:$amplitude_version",
okhttp: "com.squareup.okhttp3:okhttp:$okhttp_version"
amplitude: "com.amplitude:android-sdk:$amplitude_version",
okhttp : "com.squareup.okhttp3:okhttp:$okhttp_version"
]
anytype = [
middleware: "io.anytype:android-mw:$middleware_version"
middleware: "io.anytype:android-mw:$middleware_version"
]
}

View file

@ -4,9 +4,13 @@ import com.anytypeio.anytype.domain.base.Interactor.Status
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flatMapConcat
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.withContext
import kotlin.Result
import kotlin.coroutines.CoroutineContext
/**
@ -51,20 +55,24 @@ abstract class Interactor<in P>(
}
}
abstract class ResultInteractor<in P, R> {
operator fun invoke(params: P): Flow<R> = flow { emit(doWork(params)) }
abstract class ResultInteractor<in P, R>(
private val context: CoroutineContext = Dispatchers.IO
) {
fun asFlow(params: P): Flow<R> = flow { emit(doWork(params)) }.flowOn(context)
fun stream(params: P): Flow<Resultat<R>> {
return asFlow(params)
.map {
@Suppress("USELESS_CAST")
Resultat.Success(run(params)) as Resultat<R>
}
.onStart { emit(Resultat.Loading()) }
.catch { e -> emit(Resultat.Failure(e)) }
.flowOn(context)
}
suspend fun run(params: P) = doWork(params)
suspend fun execute(params: P): Result<R> = runCatching { doWork(params) }
suspend fun execute(params: P): Resultat<R> = runCatchingL { doWork(params) }
protected abstract suspend fun doWork(params: P): R
}
suspend fun <R, T> Result<T>.suspendFold(
onSuccess: suspend (value: T) -> R,
onFailure: suspend (Throwable) -> R
): R {
return when (val exception = exceptionOrNull()) {
null -> onSuccess(getOrNull() as T)
else -> onFailure(exception)
}
}
}

View file

@ -2,6 +2,10 @@ package com.anytypeio.anytype.domain.base
import com.anytypeio.anytype.domain.error.Error
@Deprecated(
message = "Result is deprecated",
replaceWith = ReplaceWith("Resultat", "com.anytypeio.anytype.domain.base")
)
sealed class Result<T> {
data class Success<T>(val data: T) : Result<T>()
data class Failure<T>(val error: Error) : Result<T>()

View file

@ -0,0 +1,339 @@
package com.anytypeio.anytype.domain.base
import kotlin.Result
/*
* Copyright 2022 Nicolas Haan.
* Copyright 2010-2018 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.md file.
*/
/**
* A sealed class that encapsulates a successful outcome with a value of type [T]
* or a failure with an arbitrary [Throwable] exception,
* or a loading state
* @see: This is a fork of Kotlin [kotlin.Result] class with an additional Loading state,
* but preserving Result API
*/
sealed class Resultat<out T> {
/**
* This type represent a successful outcome.
* @param value The encapsulated successful value
*/
data class Success<out T>(val value: T) : Resultat<T>() {
override fun toString(): String = "Success($value)"
}
/**
* This type represents a failed outcome.
* @param exception The encapsulated exception value
*/
data class Failure(val exception: Throwable) : Resultat<Nothing>() {
override fun toString(): String = "Failure($exception)"
}
/**
* This type represents a loading state.
*/
class Loading : Resultat<Nothing>() {
override fun toString(): String = "Loading"
}
// discovery
/**
* Returns `true` if this instance represents a successful outcome.
* In this case [isFailure] returns `false`.
* In this case [isLoading] returns `false`.
*/
val isSuccess: Boolean get() = this is Success
/**
* Returns `true` if this instance represents a failed outcome.
* In this case [isSuccess] returns `false`.
* In this case [isLoading] returns `false`.
*/
val isFailure: Boolean get() = this is Failure
/**
* Returns `true` if this instance represents a loading outcome.
* In this case [isSuccess] returns `false`.
* In this case [isFailure] returns `false`.
*/
val isLoading: Boolean get() = this is Loading
// value & exception retrieval
/**
* Returns the encapsulated value if this instance represents [success][Resultat.isSuccess] or `null`
* if it is [failure][Resultat.isFailure] or [Resultat.Loading].
*/
inline fun getOrNull(): T? =
when (this) {
is Failure -> null
is Loading -> null
is Success -> value
}
/**
* Returns the encapsulated [Throwable] exception if this instance represents [failure][isFailure] or `null`
* if it is [success][isSuccess] or [loading][isLoading].
*/
inline fun exceptionOrNull(): Throwable? =
when (this) {
is Failure -> exception
is Success -> null
is Loading -> null
}
// companion with constructors
/**
* Companion object for [Resultat] class that contains its constructor functions
* [success], [loading] and [failure].
*/
companion object {
/**
* Returns an instance that encapsulates the given [value] as successful value.
*/
inline fun <T> success(value: T): Resultat<T> = Success(value)
/**
* Returns an instance that encapsulates the given [Throwable] [exception] as failure.
*/
inline fun <T> failure(exception: Throwable): Resultat<T> = Failure(exception)
/**
* Returns an instance that represents the loading state.
*/
inline fun <T> loading(): Resultat<T> = Loading()
}
}
private val loadingException = Throwable("No value available: Loading")
/**
* Calls the specified function [block] with `this` value as its receiver and returns its encapsulated result if invocation was successful,
* catching any [Throwable] exception that was thrown from the [block] function execution and encapsulating it as a failure.
*/
inline fun <T, R> T.runCatchingL(block: T.() -> R): Resultat<R> {
return try {
Resultat.success(block())
} catch (e: Throwable) {
Resultat.failure(e)
}
}
// -- extensions ---
/**
* Returns the encapsulated value if this instance represents [success][Resultat.isSuccess] or throws the encapsulated [Throwable] exception
* if it is [failure][Resultat.isFailure].
*
* This function is a shorthand for `getOrElse { throw it }` (see [getOrElse]).
*/
fun <T> Resultat<T>.getOrThrow(): T {
when (this) {
is Resultat.Failure -> throw exception
is Resultat.Loading -> throw loadingException
is Resultat.Success -> return value
}
}
/**
* Returns the encapsulated value if this instance represents [success][Resultat.isSuccess] or the
* result of [onFailure] function for the encapsulated [Throwable] exception if it is [failure][Resultat.isFailure]
* or is [loading][Resultat.Loading].
*
* Note, that this function rethrows any [Throwable] exception thrown by [onFailure] function.
*
*/
fun <R, T : R> Resultat<T>.getOrElse(onFailure: (exception: Throwable) -> R): R {
return when (this) {
is Resultat.Failure -> onFailure(exception)
is Resultat.Loading -> onFailure(loadingException)
is Resultat.Success -> value
}
}
/**
* Returns the encapsulated value if this instance represents [success][Resultat.isSuccess] or the
* [defaultValue] if it is [failure][Resultat.isFailure] or [loading][Resultat.isLoading].
*
* This function is a shorthand for `getOrElse { defaultValue }` (see [getOrElse]).
*/
inline fun <R, T : R> Resultat<T>.getOrDefault(defaultValue: R): R {
return when (this) {
is Resultat.Failure -> defaultValue
is Resultat.Loading -> defaultValue
is Resultat.Success -> value
}
}
/**
* Returns the result of [onSuccess] for the encapsulated value if this instance represents [success][Resultat.isSuccess]
* or the result of [onFailure] function for the encapsulated [Throwable] exception if it is [failure][Resultat.isFailure].
* or the result of [onLoading] function if it is [loading][Resultat.isLoading].
*
* Note, that this function rethrows any [Throwable] exception thrown by [onSuccess] or by [onFailure] function.
*/
inline fun <T> Resultat<T>.fold(
onSuccess: (value: T) -> Unit,
onFailure: (exception: Throwable) -> Unit = {},
onLoading: () -> Unit = {},
) {
return when (this) {
is Resultat.Failure -> onFailure(exception)
is Resultat.Loading -> onLoading()
is Resultat.Success -> onSuccess(value)
}
}
suspend fun <T> Resultat<T>.suspendFold(
onSuccess: suspend (value: T) -> Unit,
onFailure: suspend (Throwable) -> Unit = {},
onLoading: suspend () -> Unit = {},
): Unit {
return when (this) {
is Resultat.Failure -> onFailure(exception)
is Resultat.Loading -> onLoading()
is Resultat.Success -> onSuccess(value)
}
}
// transformation
/**
* Returns the encapsulated result of the given [transform] function applied to the encapsulated value
* if this instance represents [success][Resultat.isSuccess] or the
* original encapsulated [Throwable] exception if it is [failure][Resultat.isFailure] or [loading][Resultat.Loading].
*
* Note, that this function rethrows any [Throwable] exception thrown by [transform] function.
* See [mapCatching] for an alternative that encapsulates exceptions.
*/
inline fun <R, T> Resultat<T>.map(transform: (value: T) -> R): Resultat<R> {
return when (this) {
is Resultat.Failure -> this
is Resultat.Success -> Resultat.success(transform(value))
is Resultat.Loading -> this
}
}
/**
* Returns the encapsulated result of the given [transform] function applied to the encapsulated value
* if this instance represents [success][Resultat.isSuccess] or the
* original encapsulated [Throwable] exception if it is [failure][Resultat.isFailure]
* or the loading state if it [loading][Resultat.Loading].
*
* This function catches any [Throwable] exception thrown by [transform] function and encapsulates it as a failure.
* See [map] for an alternative that rethrows exceptions from `transform` function.
*/
inline fun <R, T> Resultat<T>.mapCatching(transform: (value: T) -> R): Resultat<R> {
return when (this) {
is Resultat.Failure -> Resultat.Failure(exception)
is Resultat.Success -> runCatchingL { transform(value) }
is Resultat.Loading -> this
}
}
/**
* Returns the encapsulated result of the given [transform] function applied to the encapsulated [Throwable] exception
* if this instance represents [failure][Resultat.isFailure] or the
* original encapsulated value if it is [success][Resultat.isSuccess].
*
* @param recoverLoading Whether loading state calls transform or exposes untouched loading state
* Note, that this function rethrows any [Throwable] exception thrown by [transform] function.
* See [recoverCatching] for an alternative that encapsulates exceptions.
*/
inline fun <R, T : R> Resultat<T>.recover(
recoverLoading: Boolean = false,
transform: (exception: Throwable) -> R,
): Resultat<R> {
return when (this) {
is Resultat.Success -> this
is Resultat.Failure -> Resultat.success(transform(exception))
is Resultat.Loading -> if (!recoverLoading) {
this
} else {
Resultat.success(transform(Throwable("No value available: Loading")))
}
}
}
/**
* Returns the encapsulated result of the given [transform] function applied to the encapsulated [Throwable] exception
* if this instance represents [failure][Resultat.isFailure] or the
* original encapsulated value if it is [success][Resultat.isSuccess].
*
* @param recoverLoading Whether loading state calls transform or exposes untouched loading state
* This function catches any [Throwable] exception thrown by [transform] function and encapsulates it as a failure.
* See [recover] for an alternative that rethrows exceptions.
*/
inline fun <R, T : R> Resultat<T>.recoverCatching(
recoverLoading: Boolean = false,
transform: (exception: Throwable) -> R,
): Resultat<R> {
return when (this) {
is Resultat.Success -> this
is Resultat.Failure -> runCatchingL { transform(exception) }
is Resultat.Loading -> if (!recoverLoading) {
this
} else {
runCatchingL { transform(Throwable("No value available: Loading")) }
}
}
}
// "peek" onto value/exception and pipe
/**
* Performs the given [action] on the encapsulated [Throwable] exception if this instance represents [failure][Resultat.isFailure].
* Returns the original `Resultat` unchanged.
*/
inline fun <T> Resultat<T>.onFailure(action: (exception: Throwable) -> Unit): Resultat<T> {
exceptionOrNull()?.let { action(it) }
return this
}
/**
* Performs the given [action] on the encapsulated value if this instance represents [success][Resultat.isSuccess].
* Returns the original `Resultat` unchanged.
*/
inline fun <T> Resultat<T>.onSuccess(action: (value: T) -> Unit): Resultat<T> {
if (this is Resultat.Success) action(value)
return this
}
/**
* Performs the given [action] on the encapsulated value if this instance represents [success][Resultat.isLoading].
* Returns the original `Resultat` unchanged.
*/
inline fun <T> Resultat<T>.onLoading(action: () -> Unit): Resultat<T> {
if (this is Resultat.Loading) action()
return this
}
/**
* Convert Kotlin [Result] to Resultat type
*/
fun <T> Result<T>.toResultat(): Resultat<T> = fold(
onFailure = {
Resultat.failure(it)
}, onSuccess = {
Resultat.success(it)
}
)
/**
* Convert [Resultat] to Kotlin [Result]
* if [Resultat] is [Resultat.Loading], null is returned
*/
fun <T> Resultat<T>.toResult(): Result<T>? {
return when (this) {
is Resultat.Loading -> null
is Resultat.Success -> Result.success(this.value)
is Resultat.Failure -> Result.failure(this.exception)
}
}

View file

@ -230,6 +230,9 @@ interface BlockRepository {
suspend fun deleteRelationFromObject(ctx: Id, relation: Key): Payload
suspend fun debugSync(): String
suspend fun debugTree(objectId: Id, path: String): String
suspend fun debugLocalStore(path: String): String
suspend fun turnInto(

View file

@ -1,12 +1,9 @@
package com.anytypeio.anytype.domain.debugging
import com.anytypeio.anytype.domain.base.BaseUseCase
import com.anytypeio.anytype.domain.base.Either
import com.anytypeio.anytype.domain.base.ResultInteractor
import com.anytypeio.anytype.domain.block.repo.BlockRepository
class DebugSync(private val repo: BlockRepository) : BaseUseCase<String, Unit>() {
class DebugSync(private val repo: BlockRepository) : ResultInteractor<Unit, String>() {
override suspend fun run(params: Unit): Either<Throwable, String> = safe {
repo.debugSync()
}
override suspend fun doWork(params: Unit): String = repo.debugSync()
}

View file

@ -0,0 +1,16 @@
package com.anytypeio.anytype.domain.debugging
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.domain.base.ResultInteractor
import com.anytypeio.anytype.domain.block.repo.BlockRepository
class DebugTree(private val repo: BlockRepository) : ResultInteractor<DebugTree.Params, Id>() {
override suspend fun doWork(params: Params): Id =
repo.debugTree(objectId = params.objectId, path = params.path)
data class Params(
val objectId: Id,
val path: String
)
}

View file

@ -27,7 +27,7 @@ class CreateNewObject(
)
}
override suspend fun doWork(params: Unit) = getDefaultEditorType(Unit)
override suspend fun doWork(params: Unit) = getDefaultEditorType.asFlow(Unit)
.map { it.type }
.catch { emit(null) }
.map { type ->

View file

@ -97,7 +97,7 @@ class CreateNewObjectTest {
private fun givenGetDefaultObjectType(type: String? = null, name: String? = null) {
getDefaultEditorType.stub {
onBlocking { invoke(Unit) } doReturn flow {
onBlocking { asFlow(Unit) } doReturn flow {
emit(
GetDefaultEditorType.Response(
type,

View file

@ -418,6 +418,10 @@ class BlockMiddleware(
)
override suspend fun debugSync(): String = middleware.debugSync()
override suspend fun debugTree(objectId: Id, path: String): String =
middleware.debugTree(objectId = objectId, path = path)
override suspend fun debugLocalStore(path: String): String =
middleware.debugExportLocalStore(path)

View file

@ -43,6 +43,7 @@ class Middleware(
private val service: MiddlewareService,
private val factory: MiddlewareFactory,
private val logger: MiddlewareProtobufLogger,
private val protobufConverter: ProtobufConverterProvider,
) {
@Throws(Exception::class)
@ -818,7 +819,16 @@ class Middleware(
if (BuildConfig.DEBUG) logRequest(request)
val response = service.debugSync(request)
if (BuildConfig.DEBUG) logResponse(response)
return response.toString()
return protobufConverter.provideConverter().toJson(response)
}
@Throws(Exception::class)
fun debugTree(objectId: Id, path: String): String {
val request = Rpc.Debug.Tree.Request(objectId = objectId, path = path)
if (BuildConfig.DEBUG) logRequest(request)
val response = service.debugTree(request)
if (BuildConfig.DEBUG) logResponse(response)
return response.filename
}
@Throws(Exception::class)

View file

@ -362,6 +362,9 @@ interface MiddlewareService {
@Throws(Exception::class)
fun debugSync(request: Rpc.Debug.Sync.Request): Rpc.Debug.Sync.Response
@Throws(Exception::class)
fun debugTree(request: Rpc.Debug.Tree.Request): Rpc.Debug.Tree.Response
@Throws(Exception::class)
fun debugExportLocalStore(request: Rpc.Debug.ExportLocalstore.Request): Rpc.Debug.ExportLocalstore.Response

View file

@ -580,6 +580,17 @@ class MiddlewareServiceImplementation @Inject constructor(
}
}
override fun debugTree(request: Rpc.Debug.Tree.Request): Rpc.Debug.Tree.Response {
val encoded = Service.debugSync(Rpc.Debug.Tree.Request.ADAPTER.encode(request))
val response = Rpc.Debug.Tree.Response.ADAPTER.decode(encoded)
val error = response.error
if (error != null && error.code != Rpc.Debug.Tree.Response.Error.Code.NULL) {
throw Exception(error.description)
} else {
return response
}
}
override fun debugSync(request: Rpc.Debug.Sync.Request): Rpc.Debug.Sync.Response {
val encoded = Service.debugSync(Rpc.Debug.Sync.Request.ADAPTER.encode(request))
val response = Rpc.Debug.Sync.Response.ADAPTER.decode(encoded)

View file

@ -43,7 +43,7 @@ class MiddlewareTest {
@Before
fun setup() {
MockitoAnnotations.openMocks(this)
middleware = Middleware(service, factory, mock())
middleware = Middleware(service, factory, mock(), mock())
}
@Test

View file

@ -27,6 +27,7 @@ import com.anytypeio.anytype.core_utils.tools.FeatureToggles
import com.anytypeio.anytype.core_utils.ui.ViewState
import com.anytypeio.anytype.core_utils.ui.ViewStateViewModel
import com.anytypeio.anytype.domain.auth.interactor.GetProfile
import com.anytypeio.anytype.domain.base.fold
import com.anytypeio.anytype.domain.block.interactor.Move
import com.anytypeio.anytype.domain.config.GetConfig
import com.anytypeio.anytype.domain.config.GetDebugSettings

View file

@ -55,6 +55,7 @@ import com.anytypeio.anytype.core_utils.ui.ViewStateViewModel
import com.anytypeio.anytype.domain.`object`.ConvertObjectToSet
import com.anytypeio.anytype.domain.`object`.UpdateDetail
import com.anytypeio.anytype.domain.base.Result
import com.anytypeio.anytype.domain.base.fold
import com.anytypeio.anytype.domain.block.interactor.RemoveLinkMark
import com.anytypeio.anytype.domain.block.interactor.UpdateLinkMarks
import com.anytypeio.anytype.domain.block.interactor.sets.CreateObjectSet

View file

@ -14,6 +14,7 @@ import com.anytypeio.anytype.core_utils.ext.switchToLatestFrom
import com.anytypeio.anytype.core_utils.ext.withLatestFrom
import com.anytypeio.anytype.core_utils.ui.ViewStateViewModel
import com.anytypeio.anytype.domain.base.Result
import com.anytypeio.anytype.domain.base.fold
import com.anytypeio.anytype.domain.editor.Editor
import com.anytypeio.anytype.domain.error.Error
import com.anytypeio.anytype.domain.event.interactor.InterceptEvents

View file

@ -3,6 +3,7 @@ package com.anytypeio.anytype.presentation.objects
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.domain.base.fold
import com.anytypeio.anytype.domain.page.CreatePage
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableSharedFlow

View file

@ -5,6 +5,7 @@ import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Payload
import com.anytypeio.anytype.domain.`object`.DuplicateObject
import com.anytypeio.anytype.domain.base.fold
import com.anytypeio.anytype.domain.dashboard.interactor.AddToFavorite
import com.anytypeio.anytype.domain.dashboard.interactor.RemoveFromFavorite
import com.anytypeio.anytype.domain.misc.UrlBuilder

View file

@ -28,6 +28,7 @@ import com.anytypeio.anytype.core_utils.common.EventWrapper
import com.anytypeio.anytype.core_utils.ext.cancel
import com.anytypeio.anytype.domain.`object`.UpdateDetail
import com.anytypeio.anytype.domain.base.Result
import com.anytypeio.anytype.domain.base.fold
import com.anytypeio.anytype.domain.block.interactor.UpdateText
import com.anytypeio.anytype.domain.cover.SetDocCoverImage
import com.anytypeio.anytype.domain.dataview.SetDataViewSource

View file

@ -12,6 +12,7 @@ import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.SmartBlockType
import com.anytypeio.anytype.domain.base.BaseUseCase
import com.anytypeio.anytype.domain.base.Interactor
import com.anytypeio.anytype.domain.base.fold
import com.anytypeio.anytype.domain.device.ClearFileCache
import com.anytypeio.anytype.domain.launch.GetDefaultEditorType
import com.anytypeio.anytype.domain.launch.SetDefaultEditorType

View file

@ -19,6 +19,7 @@ import com.anytypeio.anytype.domain.auth.interactor.LaunchAccount
import com.anytypeio.anytype.domain.auth.interactor.LaunchWallet
import com.anytypeio.anytype.domain.auth.model.AuthStatus
import com.anytypeio.anytype.domain.base.BaseUseCase
import com.anytypeio.anytype.domain.base.fold
import com.anytypeio.anytype.domain.launch.GetDefaultEditorType
import com.anytypeio.anytype.domain.launch.SetDefaultEditorType
import com.anytypeio.anytype.domain.misc.AppActionManager

View file

@ -33,7 +33,7 @@ class TemplateViewModel(
fun onStart(ctx: Id) {
viewModelScope.launch {
state.value = openTemplate(OpenTemplate.Params(ctx))
state.value = openTemplate.asFlow(OpenTemplate.Params(ctx))
.map { result ->
when(result) {
is Result.Failure -> {

View file

@ -7,7 +7,6 @@ import com.anytypeio.anytype.core_models.Hash
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.base.ResultInteractor
import com.anytypeio.anytype.domain.block.repo.BlockRepository
import com.anytypeio.anytype.presentation.util.TEMPORARY_DIRECTORY_NAME
import kotlinx.coroutines.withContext
import java.io.File

View file

@ -3921,14 +3921,10 @@ open class EditorViewModelTest {
private fun stubGetDefaultObjectType(type: String? = null, name: String? = null) {
getDefaultEditorType.stub {
onBlocking { invoke(Unit) } doReturn flow {
emit(
GetDefaultEditorType.Response(
type,
name
)
)
}
onBlocking { run(Unit) } doReturn GetDefaultEditorType.Response(
type,
name
)
}
}

View file

@ -33,6 +33,7 @@ dependencies {
implementation project(':core-ui')
implementation project(':analytics')
implementation project(':core-models')
implementation project(':core-utils')
def applicationDependencies = rootProject.ext.mainApplication
def unitTestDependencies = rootProject.ext.unitTesting

View file

@ -33,11 +33,17 @@ fun AccountAndDataScreen(
onClearFileCachedClicked: () -> Unit,
onDeleteAccountClicked: () -> Unit,
onLogoutClicked: () -> Unit,
onDebugSyncReportClicked: () -> Unit,
isLogoutInProgress: Boolean,
isClearCacheInProgress: Boolean
isClearCacheInProgress: Boolean,
isDebugSyncReportInProgress: Boolean,
isShowDebug: Boolean,
) {
Column {
Box(Modifier.padding(vertical = 6.dp).align(Alignment.CenterHorizontally)) {
Box(
Modifier
.padding(vertical = 6.dp)
.align(Alignment.CenterHorizontally)) {
Dragger()
}
Toolbar(stringResource(R.string.account_and_data))
@ -56,18 +62,27 @@ fun AccountAndDataScreen(
isInProgress = isClearCacheInProgress
)
Divider()
if (isShowDebug) {
ActionWithProgressBar(
name = stringResource(R.string.debug_sync_report),
color = colorResource(R.color.text_primary),
onClick = onDebugSyncReportClicked,
isInProgress = isDebugSyncReportInProgress
)
Divider()
}
Section(stringResource(R.string.account))
Action(
name = stringResource(R.string.delete_account),
color = colorResource(R.color.text_primary),
onClick = onDeleteAccountClicked
name = stringResource(R.string.delete_account),
color = colorResource(R.color.text_primary),
onClick = onDeleteAccountClicked
)
Divider()
ActionWithProgressBar(
name = stringResource(R.string.log_out),
color = colorResource(R.color.palette_dark_red),
onClick = onLogoutClicked,
isInProgress = isLogoutInProgress
name = stringResource(R.string.log_out),
color = colorResource(R.color.palette_dark_red),
onClick = onLogoutClicked,
isInProgress = isLogoutInProgress
)
Divider()
Box(Modifier.height(54.dp))
@ -77,7 +92,9 @@ fun AccountAndDataScreen(
@Composable
fun Section(name: String) {
Box(
modifier = Modifier.height(52.dp).fillMaxWidth(),
modifier = Modifier
.height(52.dp)
.fillMaxWidth(),
contentAlignment = Alignment.BottomStart
) {
Text(
@ -98,7 +115,9 @@ fun Pincode(
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.height(52.dp).clickable(onClick = onClick)
modifier = Modifier
.height(52.dp)
.clickable(onClick = onClick)
) {
Image(
painterResource(R.drawable.ic_pin_code),
@ -138,7 +157,10 @@ fun Action(
onClick: () -> Unit = {}
) {
Box(
modifier = Modifier.height(52.dp).fillMaxWidth().clickable(onClick = onClick),
modifier = Modifier
.height(52.dp)
.fillMaxWidth()
.clickable(onClick = onClick),
contentAlignment = Alignment.CenterStart
) {
Text(
@ -160,7 +182,10 @@ fun ActionWithProgressBar(
isInProgress: Boolean
) {
Box(
modifier = Modifier.height(52.dp).fillMaxWidth().clickable(onClick = onClick),
modifier = Modifier
.height(52.dp)
.fillMaxWidth()
.clickable(onClick = onClick),
contentAlignment = Alignment.CenterStart
) {
Text(
@ -173,7 +198,10 @@ fun ActionWithProgressBar(
)
if (isInProgress) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.CenterEnd).padding(end = 20.dp).size(24.dp),
modifier = Modifier
.align(Alignment.CenterEnd)
.padding(end = 20.dp)
.size(24.dp),
color = colorResource(R.color.shape_secondary)
)
}

View file

@ -1,5 +1,6 @@
package com.anytypeio.anytype.ui_settings.account
import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
@ -9,7 +10,10 @@ import com.anytypeio.anytype.analytics.base.sendEvent
import com.anytypeio.anytype.domain.account.DeleteAccount
import com.anytypeio.anytype.domain.base.BaseUseCase
import com.anytypeio.anytype.domain.base.Interactor
import com.anytypeio.anytype.domain.base.fold
import com.anytypeio.anytype.domain.device.ClearFileCache
import com.anytypeio.anytype.ui_settings.account.repo.DebugSyncShareDownloader
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
@ -17,14 +21,19 @@ import timber.log.Timber
class AccountAndDataViewModel(
private val clearFileCache: ClearFileCache,
private val analytics: Analytics,
private val deleteAccount: DeleteAccount
private val deleteAccount: DeleteAccount,
private val debugSyncShareDownloader: DebugSyncShareDownloader,
) : ViewModel() {
private val jobs = mutableListOf<Job>()
val isClearFileCacheInProgress = MutableStateFlow(false)
val isDebugSyncReportInProgress = MutableStateFlow(false)
val isLoggingOut = MutableStateFlow(false)
val debugSyncReportUri = MutableStateFlow<Uri?>(null)
fun onClearFileCacheAccepted() {
viewModelScope.launch {
jobs += viewModelScope.launch {
clearFileCache(BaseUseCase.None).collect { status ->
when (status) {
is Interactor.Status.Started -> {
@ -32,7 +41,6 @@ class AccountAndDataViewModel(
}
is Interactor.Status.Error -> {
isClearFileCacheInProgress.value = false
val msg = "Error while clearing file cache: ${status.throwable.message}"
Timber.e(status.throwable, "Error while clearing file cache")
// TODO send toast
}
@ -49,14 +57,14 @@ class AccountAndDataViewModel(
}
fun onClearCacheButtonClicked() {
viewModelScope.sendEvent(
jobs += viewModelScope.sendEvent(
analytics = analytics,
eventName = EventsDictionary.fileOffloadScreenShow
)
}
fun onDeleteAccountClicked() {
viewModelScope.launch {
jobs += viewModelScope.launch {
deleteAccount(BaseUseCase.None).process(
success = {
sendEvent(
@ -72,16 +80,45 @@ class AccountAndDataViewModel(
}
}
fun onDebugSyncReportClicked() {
jobs += viewModelScope.launch {
debugSyncShareDownloader.stream(Unit).collect { result ->
result.fold(
onSuccess = { report ->
isDebugSyncReportInProgress.value = false
debugSyncReportUri.value = report
Timber.d(report.toString())
},
onLoading = { isDebugSyncReportInProgress.value = true },
onFailure = { e ->
isDebugSyncReportInProgress.value = false
Timber.e(e, "Error while creating a debug sync report")
}
)
}
}
}
fun onStop() {
Timber.d("onStop, ")
jobs.apply {
forEach { it.cancel() }
clear()
}
}
class Factory(
private val clearFileCache: ClearFileCache,
private val deleteAccount: DeleteAccount,
private val analytics: Analytics
private val debugSyncShareDownloader: DebugSyncShareDownloader,
private val analytics: Analytics,
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return AccountAndDataViewModel(
clearFileCache = clearFileCache,
deleteAccount = deleteAccount,
debugSyncShareDownloader = debugSyncShareDownloader,
analytics = analytics
) as T
}

View file

@ -0,0 +1,26 @@
package com.anytypeio.anytype.ui_settings.account.repo
import android.net.Uri
import com.anytypeio.anytype.domain.base.ResultInteractor
import com.anytypeio.anytype.domain.debugging.DebugSync
import java.text.SimpleDateFormat
import java.util.*
class DebugSyncShareDownloader(
private val debugSync: DebugSync,
private val fileSaver: FileSaver,
) : ResultInteractor<Unit, Uri>() {
private fun getFileName(): String {
val date = Calendar.getInstance().time
val dateFormat = SimpleDateFormat("dd-MM-yyyy-HH:mm:ss", Locale.getDefault())
val formattedDate = dateFormat.format(date)
val fileName = "DebugSync$formattedDate.txt"
return fileName
}
override suspend fun doWork(params: Unit): Uri {
val content = debugSync.run(Unit)
return fileSaver.run(FileSaver.Params(content = content, name = getFileName()))
}
}

View file

@ -0,0 +1,45 @@
package com.anytypeio.anytype.ui_settings.account.repo
import android.content.Context
import android.net.Uri
import com.anytypeio.anytype.domain.base.ResultInteractor
import com.anytypeio.anytype.presentation.util.downloader.UriFileProvider
import java.io.File
import java.io.FileOutputStream
class FileSaver(
private val context: Context,
private val uriFileProvider: UriFileProvider
) : ResultInteractor<FileSaver.Params, Uri>() {
data class Params(
val content: String,
val name: String
)
override suspend fun doWork(params: Params): Uri {
val cacheDir = context.cacheDir
require(cacheDir != null) { "Impossible to cache files!" }
val downloadFolder = File("${cacheDir.path}/debug_sync/").apply { mkdirs() }
val resultFilePath = "${cacheDir.path}/${params.name}"
val resultFile = File(resultFilePath)
if (!resultFile.exists()) {
val tempFileFolderPath = "${downloadFolder.absolutePath}/tmp"
val tempDir = File(tempFileFolderPath)
if (tempDir.exists()) tempDir.deleteRecursively()
tempDir.mkdirs()
val tempResult = File(tempFileFolderPath, params.name)
FileOutputStream(tempResult).use {
it.write(params.content.toByteArray())
}
tempResult.renameTo(resultFile)
}
return uriFileProvider.getUriForFile(resultFile)
}
}

View file

@ -10,6 +10,7 @@
<string name="appearance">Appearance</string>
<string name="recovery_phrase">Recovery phrase</string>
<string name="clear_file_cache">Clear file cache</string>
<string name="debug_sync_report">Debug Sync Report</string>
<string name="account">Account</string>
<string name="reset_account">Reset account</string>
<string name="delete_account">Delete account</string>