diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 6d01f6fd..0b6a0042 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -9,19 +9,38 @@ android {
defaultConfig {
applicationId = logFoxPackageName
- versionCode = 62
- versionName = "2.0.2"
+ versionCode = 63
+ versionName = "2.0.3"
}
}
dependencies {
- implementation(projects.feature.crashes)
- implementation(projects.feature.filters)
- implementation(projects.feature.logging)
- implementation(projects.feature.recordings)
+ implementation(projects.feature.appsPicker.api)
+ implementation(projects.feature.appsPicker.impl)
+
+ implementation(projects.feature.crashes.appsList)
+ implementation(projects.feature.crashes.details)
+ implementation(projects.feature.crashes.impl)
+ implementation(projects.feature.crashes.list)
+
+ implementation(projects.feature.filters.edit)
+ implementation(projects.feature.filters.impl)
+ implementation(projects.feature.filters.list)
+
+ implementation(projects.feature.logging.extendedCopy)
+ implementation(projects.feature.logging.impl)
+ implementation(projects.feature.logging.list)
+ implementation(projects.feature.logging.search)
+ implementation(projects.feature.logging.service)
+
+ implementation(projects.feature.recordings.details)
+ implementation(projects.feature.recordings.impl)
+ implementation(projects.feature.recordings.list)
+
implementation(projects.feature.settings)
implementation(projects.feature.setup)
+ implementation(libs.timber)
implementation(libs.viewpump)
implementation(libs.coil)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index cc226962..474177c5 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -14,10 +14,11 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.LogFox"
- android:localeConfig="@xml/locales_config">
+ android:localeConfig="@xml/locales_config"
+ android:enableOnBackInvokedCallback="true">
@@ -29,7 +30,7 @@
{
+ ) : Fetcher.Factory {
override fun create(
- data: InstalledApp,
+ data: com.f0x1d.logfox.feature.apps.picker.InstalledApp,
options: Options,
imageLoader: ImageLoader,
): Fetcher = AppIconFetcher(
diff --git a/app/src/main/kotlin/com/f0x1d/logfox/di/logs/MainActivityPendingIntentProviderModule.kt b/app/src/main/kotlin/com/f0x1d/logfox/di/logs/MainActivityPendingIntentProviderModule.kt
index 2fcaa6ce..c0ffa0a0 100644
--- a/app/src/main/kotlin/com/f0x1d/logfox/di/logs/MainActivityPendingIntentProviderModule.kt
+++ b/app/src/main/kotlin/com/f0x1d/logfox/di/logs/MainActivityPendingIntentProviderModule.kt
@@ -2,9 +2,9 @@ package com.f0x1d.logfox.di.logs
import android.app.PendingIntent
import android.content.Context
-import com.f0x1d.feature.logging.service.MainActivityPendingIntentProvider
import com.f0x1d.logfox.arch.makeActivityPendingIntent
-import com.f0x1d.logfox.ui.activity.MainActivity
+import com.f0x1d.logfox.feature.logging.service.presentation.MainActivityPendingIntentProvider
+import com.f0x1d.logfox.presentation.ui.activity.MainActivity
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
diff --git a/app/src/main/kotlin/com/f0x1d/logfox/di/settings/LoggingServiceDelegateModule.kt b/app/src/main/kotlin/com/f0x1d/logfox/di/settings/LoggingServiceDelegateModule.kt
deleted file mode 100644
index 7269f5eb..00000000
--- a/app/src/main/kotlin/com/f0x1d/logfox/di/settings/LoggingServiceDelegateModule.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.f0x1d.logfox.di.settings
-
-import android.content.Context
-import com.f0x1d.feature.logging.service.LoggingService
-import com.f0x1d.logfox.arch.sendService
-import com.f0x1d.logfox.feature.settings.LoggingServiceDelegate
-import dagger.Binds
-import dagger.Module
-import dagger.hilt.InstallIn
-import dagger.hilt.android.qualifiers.ApplicationContext
-import dagger.hilt.components.SingletonComponent
-import javax.inject.Inject
-
-@Module
-@InstallIn(SingletonComponent::class)
-internal interface LoggingServiceDelegateModule {
-
- @Binds
- fun bindLoggingServiceDelegate(
- loggingServiceDelegateImpl: LoggingServiceDelegateImpl,
- ): LoggingServiceDelegate
-}
-
-internal class LoggingServiceDelegateImpl @Inject constructor(
- @ApplicationContext private val context: Context,
-) : LoggingServiceDelegate {
- override fun restartLogging() {
- context.sendService(action = LoggingService.ACTION_RESTART_LOGGING)
- }
-}
diff --git a/app/src/main/kotlin/com/f0x1d/logfox/presentation/MainAction.kt b/app/src/main/kotlin/com/f0x1d/logfox/presentation/MainAction.kt
new file mode 100644
index 00000000..a96e6e12
--- /dev/null
+++ b/app/src/main/kotlin/com/f0x1d/logfox/presentation/MainAction.kt
@@ -0,0 +1,5 @@
+package com.f0x1d.logfox.presentation
+
+sealed interface MainAction {
+ data object OpenSetup : MainAction
+}
diff --git a/app/src/main/kotlin/com/f0x1d/logfox/presentation/MainState.kt b/app/src/main/kotlin/com/f0x1d/logfox/presentation/MainState.kt
new file mode 100644
index 00000000..25255c9c
--- /dev/null
+++ b/app/src/main/kotlin/com/f0x1d/logfox/presentation/MainState.kt
@@ -0,0 +1,3 @@
+package com.f0x1d.logfox.presentation
+
+data object MainState
diff --git a/app/src/main/kotlin/com/f0x1d/logfox/viewmodel/MainViewModel.kt b/app/src/main/kotlin/com/f0x1d/logfox/presentation/MainViewModel.kt
similarity index 77%
rename from app/src/main/kotlin/com/f0x1d/logfox/viewmodel/MainViewModel.kt
rename to app/src/main/kotlin/com/f0x1d/logfox/presentation/MainViewModel.kt
index 3640e272..731cd453 100644
--- a/app/src/main/kotlin/com/f0x1d/logfox/viewmodel/MainViewModel.kt
+++ b/app/src/main/kotlin/com/f0x1d/logfox/presentation/MainViewModel.kt
@@ -1,12 +1,10 @@
-package com.f0x1d.logfox.viewmodel
+package com.f0x1d.logfox.presentation
import android.app.Application
import android.content.Intent
-import com.f0x1d.feature.logging.service.LoggingService
import com.f0x1d.logfox.arch.hasPermissionToReadLogs
import com.f0x1d.logfox.arch.startForegroundServiceAvailable
import com.f0x1d.logfox.arch.viewmodel.BaseViewModel
-import com.f0x1d.logfox.arch.viewmodel.Event
import com.f0x1d.logfox.preferences.shared.AppPreferences
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@@ -15,8 +13,10 @@ import javax.inject.Inject
class MainViewModel @Inject constructor(
private val appPreferences: AppPreferences,
application: Application,
-): BaseViewModel(application) {
-
+) : BaseViewModel(
+ initialStateProvider = { MainState },
+ application = application,
+) {
var askedNotificationsPermission
get() = appPreferences.askedNotificationsPermission
set(value) { appPreferences.askedNotificationsPermission = value }
@@ -29,16 +29,14 @@ class MainViewModel @Inject constructor(
private fun load() {
if (ctx.hasPermissionToReadLogs) {
- Intent(ctx, LoggingService::class.java).let {
+ Intent(ctx, com.f0x1d.logfox.feature.logging.service.presentation.LoggingService::class.java).let {
if (startForegroundServiceAvailable)
ctx.startForegroundService(it)
else
ctx.startService(it)
}
} else {
- sendEvent(OpenSetup)
+ sendAction(MainAction.OpenSetup)
}
}
}
-
-data object OpenSetup : Event
diff --git a/app/src/main/kotlin/com/f0x1d/logfox/ui/activity/MainActivity.kt b/app/src/main/kotlin/com/f0x1d/logfox/presentation/ui/activity/MainActivity.kt
similarity index 92%
rename from app/src/main/kotlin/com/f0x1d/logfox/ui/activity/MainActivity.kt
rename to app/src/main/kotlin/com/f0x1d/logfox/presentation/ui/activity/MainActivity.kt
index 95ee0055..468cc31b 100644
--- a/app/src/main/kotlin/com/f0x1d/logfox/ui/activity/MainActivity.kt
+++ b/app/src/main/kotlin/com/f0x1d/logfox/presentation/ui/activity/MainActivity.kt
@@ -1,4 +1,4 @@
-package com.f0x1d.logfox.ui.activity
+package com.f0x1d.logfox.presentation.ui.activity
import android.Manifest
import android.annotation.SuppressLint
@@ -23,22 +23,21 @@ import com.f0x1d.logfox.arch.contrastedNavBarAvailable
import com.f0x1d.logfox.arch.gesturesAvailable
import com.f0x1d.logfox.arch.hasNotificationsPermission
import com.f0x1d.logfox.arch.isHorizontalOrientation
-import com.f0x1d.logfox.arch.ui.activity.BaseViewModelActivity
-import com.f0x1d.logfox.arch.viewmodel.Event
+import com.f0x1d.logfox.arch.presentation.ui.activity.BaseActivity
import com.f0x1d.logfox.databinding.ActivityMainBinding
import com.f0x1d.logfox.navigation.Directions
import com.f0x1d.logfox.navigation.NavGraphs
+import com.f0x1d.logfox.presentation.MainAction
+import com.f0x1d.logfox.presentation.MainViewModel
import com.f0x1d.logfox.strings.Strings
import com.f0x1d.logfox.ui.Icons
-import com.f0x1d.logfox.viewmodel.MainViewModel
-import com.f0x1d.logfox.viewmodel.OpenSetup
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
-class MainActivity: BaseViewModelActivity(), NavController.OnDestinationChangedListener {
+class MainActivity: BaseActivity(), NavController.OnDestinationChangedListener {
- override val viewModel by viewModels()
+ private val viewModel by viewModels()
private val navController by lazy {
val navHostFragment = supportFragmentManager.findFragmentById(
@@ -94,6 +93,12 @@ class MainActivity: BaseViewModelActivity(),
viewModel.askedNotificationsPermission = true
}
+
+ viewModel.actions.collectWithLifecycle { action ->
+ when (action) {
+ is MainAction.OpenSetup -> navController.navigate(Directions.action_global_setupFragment)
+ }
+ }
}
private fun ActivityMainBinding.setupNavigation() {
@@ -139,14 +144,6 @@ class MainActivity: BaseViewModelActivity(),
}
}
- override fun onEvent(event: Event) {
- super.onEvent(event)
-
- when (event) {
- is OpenSetup -> navController.navigate(Directions.action_global_setupFragment)
- }
- }
-
override fun onDestinationChanged(controller: NavController, destination: NavDestination, arguments: Bundle?) {
val barShown = when (destination.id) {
Directions.setupFragment -> false
diff --git a/app/src/main/kotlin/com/f0x1d/logfox/ui/activity/OpenFileActivity.kt b/app/src/main/kotlin/com/f0x1d/logfox/presentation/ui/activity/OpenFileActivity.kt
similarity index 91%
rename from app/src/main/kotlin/com/f0x1d/logfox/ui/activity/OpenFileActivity.kt
rename to app/src/main/kotlin/com/f0x1d/logfox/presentation/ui/activity/OpenFileActivity.kt
index ebf36e4a..580f7b58 100644
--- a/app/src/main/kotlin/com/f0x1d/logfox/ui/activity/OpenFileActivity.kt
+++ b/app/src/main/kotlin/com/f0x1d/logfox/presentation/ui/activity/OpenFileActivity.kt
@@ -1,4 +1,4 @@
-package com.f0x1d.logfox.ui.activity
+package com.f0x1d.logfox.presentation.ui.activity
import android.content.Intent
import android.os.Bundle
@@ -19,4 +19,4 @@ class OpenFileActivity: AppCompatActivity() {
)
finish()
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/kotlin/com/f0x1d/logfox/receiver/BootReceiver.kt b/app/src/main/kotlin/com/f0x1d/logfox/receiver/BootReceiver.kt
index c26d6bf2..d32ce56b 100644
--- a/app/src/main/kotlin/com/f0x1d/logfox/receiver/BootReceiver.kt
+++ b/app/src/main/kotlin/com/f0x1d/logfox/receiver/BootReceiver.kt
@@ -3,10 +3,10 @@ package com.f0x1d.logfox.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
-import com.f0x1d.feature.logging.service.LoggingService
import com.f0x1d.logfox.arch.hasPermissionToReadLogs
import com.f0x1d.logfox.arch.startForegroundServiceAvailable
import com.f0x1d.logfox.arch.toast
+import com.f0x1d.logfox.feature.logging.service.presentation.LoggingService
import com.f0x1d.logfox.preferences.shared.AppPreferences
import com.f0x1d.logfox.strings.Strings
import com.f0x1d.logfox.terminals.ShizukuTerminal
@@ -26,7 +26,7 @@ class BootReceiver: BroadcastReceiver() {
}
if (context.hasPermissionToReadLogs) {
- Intent(context, LoggingService::class.java).let {
+ Intent(context, com.f0x1d.logfox.feature.logging.service.presentation.LoggingService::class.java).let {
if (startForegroundServiceAvailable)
context.startForegroundService(it)
else
diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml
index bc0204d6..de518d82 100644
--- a/app/src/main/res/xml/file_paths.xml
+++ b/app/src/main/res/xml/file_paths.xml
@@ -1,5 +1,6 @@
+
-
\ No newline at end of file
+
diff --git a/build-logic/convention/src/main/kotlin/extensions/Dependencies.kt b/build-logic/convention/src/main/kotlin/extensions/Dependencies.kt
index 4543fbc5..d7bf9106 100644
--- a/build-logic/convention/src/main/kotlin/extensions/Dependencies.kt
+++ b/build-logic/convention/src/main/kotlin/extensions/Dependencies.kt
@@ -5,7 +5,7 @@ import org.gradle.kotlin.dsl.DependencyHandlerScope
import org.gradle.kotlin.dsl.project
internal fun DependencyHandlerScope.coreDependencies(withCompose: Boolean = true) {
- implementation(project(":data"))
+ implementation(project(":shared"))
implementation(project(":strings"))
implementation(project(":core:arch"))
@@ -43,10 +43,7 @@ internal fun DependencyHandlerScope.androidTestImplementation(dependency: Any):
)
internal fun DependencyHandlerScope.implementation(bundle: List): List = bundle.map {
- add(
- configurationName = "implementation",
- dependencyNotation = it,
- )
+ implementation(it)
}
internal fun DependencyHandlerScope.ksp(dependency: Any): Dependency? = add(
diff --git a/build-logic/convention/src/main/kotlin/main/AndroidCoreConventionPlugin.kt b/build-logic/convention/src/main/kotlin/main/AndroidCoreConventionPlugin.kt
index 4bffc49b..61b66ddb 100644
--- a/build-logic/convention/src/main/kotlin/main/AndroidCoreConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/main/AndroidCoreConventionPlugin.kt
@@ -15,9 +15,11 @@ class AndroidCoreConventionPlugin : Plugin {
}
dependencies {
- implementation(project(":data"))
+ implementation(project(":shared"))
implementation(project(":strings"))
+ implementation(library("timber"))
+
implementation(library("material"))
implementation(bundle("androidx"))
}
diff --git a/build-logic/convention/src/main/kotlin/main/feature/AndroidFeatureConventionPlugin.kt b/build-logic/convention/src/main/kotlin/main/feature/AndroidFeatureConventionPlugin.kt
index 78023b90..885772d2 100644
--- a/build-logic/convention/src/main/kotlin/main/feature/AndroidFeatureConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/main/feature/AndroidFeatureConventionPlugin.kt
@@ -19,6 +19,7 @@ class AndroidFeatureConventionPlugin : Plugin {
dependencies {
coreDependencies(withCompose = false)
+ implementation(library("timber"))
implementation(library("material"))
implementation(bundle("androidx"))
implementation(bundle("androidx-navigation"))
diff --git a/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/logs/TimberFileTree.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/logs/TimberFileTree.kt
new file mode 100644
index 00000000..44e95b37
--- /dev/null
+++ b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/logs/TimberFileTree.kt
@@ -0,0 +1,45 @@
+package com.f0x1d.logfox.arch.logs
+
+import android.content.Context
+import com.f0x1d.logfox.arch.di.IODispatcher
+import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
+import kotlinx.coroutines.launch
+import timber.log.Timber
+import java.io.File
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class TimberFileTree @Inject constructor(
+ @ApplicationContext private val context: Context,
+ @IODispatcher private val ioDispatcher: CoroutineDispatcher,
+) : Timber.DebugTree() {
+
+ private val logsFile = context.timberLogFile.apply { delete() }
+
+ private val channel = Channel(capacity = UNLIMITED)
+ private val scope = CoroutineScope(ioDispatcher)
+
+ init {
+ scope.launch {
+ for (value in channel) {
+ logsFile.appendText(
+ text = value + "\n",
+ )
+ }
+ }
+ }
+
+ override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
+ val exception = t?.stackTraceToString()?.let { "\n$it" } ?: ""
+ val line = "${tag ?: "NO_TAG"}: $message" + exception
+
+ channel.trySend(line)
+ }
+}
+
+val Context.timberLogFile: File get() = File(filesDir, "timber.log")
diff --git a/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/adapter/BaseListAdapter.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/presentation/adapter/BaseListAdapter.kt
similarity index 91%
rename from core/arch/src/main/kotlin/com/f0x1d/logfox/arch/adapter/BaseListAdapter.kt
rename to core/arch/src/main/kotlin/com/f0x1d/logfox/arch/presentation/adapter/BaseListAdapter.kt
index 803b70e6..227b95ce 100644
--- a/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/adapter/BaseListAdapter.kt
+++ b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/presentation/adapter/BaseListAdapter.kt
@@ -1,4 +1,4 @@
-package com.f0x1d.logfox.arch.adapter
+package com.f0x1d.logfox.arch.presentation.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
@@ -6,7 +6,7 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
-import com.f0x1d.logfox.arch.ui.viewholder.BaseViewHolder
+import com.f0x1d.logfox.arch.presentation.ui.viewholder.BaseViewHolder
abstract class BaseListAdapter(
diffUtil: DiffUtil.ItemCallback,
diff --git a/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/SnackbarExt.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/presentation/ui/SnackbarExt.kt
similarity index 79%
rename from core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/SnackbarExt.kt
rename to core/arch/src/main/kotlin/com/f0x1d/logfox/arch/presentation/ui/SnackbarExt.kt
index e7ba1bee..0e8a8001 100644
--- a/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/SnackbarExt.kt
+++ b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/presentation/ui/SnackbarExt.kt
@@ -1,4 +1,4 @@
-package com.f0x1d.logfox.arch.ui
+package com.f0x1d.logfox.arch.presentation.ui
import android.view.View
import com.google.android.material.snackbar.Snackbar
diff --git a/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/WindowExt.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/presentation/ui/WindowExt.kt
similarity index 97%
rename from core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/WindowExt.kt
rename to core/arch/src/main/kotlin/com/f0x1d/logfox/arch/presentation/ui/WindowExt.kt
index 53d4ae87..ad3e62c7 100644
--- a/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/WindowExt.kt
+++ b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/presentation/ui/WindowExt.kt
@@ -1,4 +1,4 @@
-package com.f0x1d.logfox.arch.ui
+package com.f0x1d.logfox.arch.presentation.ui
import android.content.Context
import android.graphics.Color
diff --git a/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/activity/BaseActivity.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/presentation/ui/activity/BaseActivity.kt
similarity index 89%
rename from core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/activity/BaseActivity.kt
rename to core/arch/src/main/kotlin/com/f0x1d/logfox/arch/presentation/ui/activity/BaseActivity.kt
index 79c1edb2..5686279c 100644
--- a/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/activity/BaseActivity.kt
+++ b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/presentation/ui/activity/BaseActivity.kt
@@ -1,12 +1,12 @@
-package com.f0x1d.logfox.arch.ui.activity
+package com.f0x1d.logfox.arch.presentation.ui.activity
import android.content.Context
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.viewbinding.ViewBinding
-import com.f0x1d.logfox.arch.ui.base.SimpleLifecycleOwner
-import com.f0x1d.logfox.arch.ui.enableEdgeToEdge
-import com.f0x1d.logfox.arch.ui.snackbar
+import com.f0x1d.logfox.arch.presentation.ui.base.SimpleLifecycleOwner
+import com.f0x1d.logfox.arch.presentation.ui.enableEdgeToEdge
+import com.f0x1d.logfox.arch.presentation.ui.snackbar
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
diff --git a/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/base/SimpleLifecycleOwner.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/presentation/ui/base/SimpleLifecycleOwner.kt
similarity index 95%
rename from core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/base/SimpleLifecycleOwner.kt
rename to core/arch/src/main/kotlin/com/f0x1d/logfox/arch/presentation/ui/base/SimpleLifecycleOwner.kt
index 9259c03f..b1fc822a 100644
--- a/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/base/SimpleLifecycleOwner.kt
+++ b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/presentation/ui/base/SimpleLifecycleOwner.kt
@@ -1,4 +1,4 @@
-package com.f0x1d.logfox.arch.ui.base
+package com.f0x1d.logfox.arch.presentation.ui.base
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
diff --git a/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/dialog/BaseBottomSheet.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/presentation/ui/dialog/BaseBottomSheetFragment.kt
similarity index 85%
rename from core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/dialog/BaseBottomSheet.kt
rename to core/arch/src/main/kotlin/com/f0x1d/logfox/arch/presentation/ui/dialog/BaseBottomSheetFragment.kt
index 0cacad25..a3f99805 100644
--- a/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/dialog/BaseBottomSheet.kt
+++ b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/presentation/ui/dialog/BaseBottomSheetFragment.kt
@@ -1,4 +1,4 @@
-package com.f0x1d.logfox.arch.ui.dialog
+package com.f0x1d.logfox.arch.presentation.ui.dialog
import android.annotation.SuppressLint
import android.app.Dialog
@@ -7,13 +7,14 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.viewbinding.ViewBinding
-import com.f0x1d.logfox.arch.ui.base.SimpleFragmentLifecycleOwner
-import com.f0x1d.logfox.arch.ui.enableEdgeToEdge
+import com.f0x1d.logfox.arch.presentation.ui.base.SimpleFragmentLifecycleOwner
+import com.f0x1d.logfox.arch.presentation.ui.enableEdgeToEdge
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
-abstract class BaseBottomSheet: BottomSheetDialogFragment(), SimpleFragmentLifecycleOwner {
+abstract class BaseBottomSheetFragment: BottomSheetDialogFragment(),
+ SimpleFragmentLifecycleOwner {
private var mutableBinding: T? = null
protected val binding: T get() = mutableBinding!!
diff --git a/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/BaseFragment.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/presentation/ui/fragment/BaseFragment.kt
similarity index 89%
rename from core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/BaseFragment.kt
rename to core/arch/src/main/kotlin/com/f0x1d/logfox/arch/presentation/ui/fragment/BaseFragment.kt
index b5dd7197..b29c989b 100644
--- a/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/BaseFragment.kt
+++ b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/presentation/ui/fragment/BaseFragment.kt
@@ -1,4 +1,4 @@
-package com.f0x1d.logfox.arch.ui.fragment
+package com.f0x1d.logfox.arch.presentation.ui.fragment
import android.content.res.Configuration
import android.os.Bundle
@@ -7,8 +7,8 @@ import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.viewbinding.ViewBinding
-import com.f0x1d.logfox.arch.ui.base.SimpleFragmentLifecycleOwner
-import com.f0x1d.logfox.arch.ui.snackbar
+import com.f0x1d.logfox.arch.presentation.ui.base.SimpleFragmentLifecycleOwner
+import com.f0x1d.logfox.arch.presentation.ui.snackbar
import dev.chrisbanes.insetter.applyInsetter
abstract class BaseFragment: Fragment(), SimpleFragmentLifecycleOwner {
diff --git a/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/compose/BaseComposeFragment.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/presentation/ui/fragment/compose/BaseComposeFragment.kt
similarity index 70%
rename from core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/compose/BaseComposeFragment.kt
rename to core/arch/src/main/kotlin/com/f0x1d/logfox/arch/presentation/ui/fragment/compose/BaseComposeFragment.kt
index 16301ef9..9d803276 100644
--- a/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/compose/BaseComposeFragment.kt
+++ b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/presentation/ui/fragment/compose/BaseComposeFragment.kt
@@ -1,4 +1,4 @@
-package com.f0x1d.logfox.arch.ui.fragment.compose
+package com.f0x1d.logfox.arch.presentation.ui.fragment.compose
import android.os.Bundle
import android.view.LayoutInflater
@@ -9,7 +9,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import com.f0x1d.logfox.arch.databinding.FragmentComposeBinding
-import com.f0x1d.logfox.arch.ui.fragment.BaseFragment
+import com.f0x1d.logfox.arch.presentation.ui.fragment.BaseFragment
abstract class BaseComposeFragment : BaseFragment() {
@@ -25,11 +25,11 @@ abstract class BaseComposeFragment : BaseFragment() {
@Composable
abstract fun Content()
-}
-internal fun ComposeView.setup(content: @Composable () -> Unit) {
- consumeWindowInsets = false
- setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+ private fun ComposeView.setup(content: @Composable () -> Unit) {
+ consumeWindowInsets = false
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
- setContent(content)
+ setContent(content)
+ }
}
diff --git a/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/viewholder/BaseViewHolder.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/presentation/ui/viewholder/BaseViewHolder.kt
similarity index 87%
rename from core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/viewholder/BaseViewHolder.kt
rename to core/arch/src/main/kotlin/com/f0x1d/logfox/arch/presentation/ui/viewholder/BaseViewHolder.kt
index 9d02d816..d9fd8724 100644
--- a/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/viewholder/BaseViewHolder.kt
+++ b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/presentation/ui/viewholder/BaseViewHolder.kt
@@ -1,8 +1,8 @@
-package com.f0x1d.logfox.arch.ui.viewholder
+package com.f0x1d.logfox.arch.presentation.ui.viewholder
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
-import com.f0x1d.logfox.arch.adapter.BaseListAdapter
+import com.f0x1d.logfox.arch.presentation.adapter.BaseListAdapter
abstract class BaseViewHolder(
protected val binding: D
diff --git a/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/repository/BaseRepository.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/repository/BaseRepository.kt
deleted file mode 100644
index 17b33596..00000000
--- a/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/repository/BaseRepository.kt
+++ /dev/null
@@ -1,3 +0,0 @@
-package com.f0x1d.logfox.arch.repository
-
-abstract class BaseRepository
diff --git a/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/activity/BaseViewModelActivity.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/activity/BaseViewModelActivity.kt
deleted file mode 100644
index 4fb01589..00000000
--- a/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/activity/BaseViewModelActivity.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.f0x1d.logfox.arch.ui.activity
-
-import android.os.Bundle
-import androidx.viewbinding.ViewBinding
-import com.f0x1d.logfox.arch.viewmodel.BaseViewModel
-import com.f0x1d.logfox.arch.viewmodel.Event
-import com.f0x1d.logfox.arch.viewmodel.ShowSnackbar
-
-abstract class BaseViewModelActivity: BaseActivity() {
-
- abstract val viewModel: T
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- viewModel.eventsFlow.collectWithLifecycle(collector = ::onEvent)
- }
-
- open fun onEvent(event: Event) {
- when (event) {
- is ShowSnackbar -> snackbar(event.text)
- }
- }
-}
diff --git a/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/dialog/BaseViewModelBottomSheet.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/dialog/BaseViewModelBottomSheet.kt
deleted file mode 100644
index f260ea79..00000000
--- a/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/dialog/BaseViewModelBottomSheet.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.f0x1d.logfox.arch.ui.dialog
-
-import android.os.Bundle
-import android.view.View
-import androidx.viewbinding.ViewBinding
-import com.f0x1d.logfox.arch.viewmodel.BaseViewModel
-import com.f0x1d.logfox.arch.viewmodel.Event
-
-abstract class BaseViewModelBottomSheet: BaseBottomSheet() {
-
- abstract val viewModel: T
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- viewModel.eventsFlow.collectWithLifecycle(collector = ::onEvent)
- }
-
- open fun onEvent(event: Event) = Unit
-}
diff --git a/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/BaseViewModelFragment.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/BaseViewModelFragment.kt
deleted file mode 100644
index af4e719c..00000000
--- a/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/BaseViewModelFragment.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.f0x1d.logfox.arch.ui.fragment
-
-import android.os.Bundle
-import android.view.View
-import androidx.viewbinding.ViewBinding
-import com.f0x1d.logfox.arch.viewmodel.BaseViewModel
-import com.f0x1d.logfox.arch.viewmodel.Event
-import com.f0x1d.logfox.arch.viewmodel.ShowSnackbar
-
-abstract class BaseViewModelFragment: BaseFragment() {
-
- abstract val viewModel: T
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- viewModel.eventsFlow.collectWithLifecycle(collector = ::onEvent)
- }
-
- open fun onEvent(event: Event) {
- when (event) {
- is ShowSnackbar -> snackbar(event.text)
- }
- }
-}
diff --git a/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/compose/BaseComposeViewModelFragment.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/compose/BaseComposeViewModelFragment.kt
deleted file mode 100644
index c4b9e94a..00000000
--- a/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/compose/BaseComposeViewModelFragment.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-package com.f0x1d.logfox.arch.ui.fragment.compose
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.compose.runtime.Composable
-import com.f0x1d.logfox.arch.databinding.FragmentComposeBinding
-import com.f0x1d.logfox.arch.ui.fragment.BaseViewModelFragment
-import com.f0x1d.logfox.arch.ui.snackbar
-import com.f0x1d.logfox.arch.viewmodel.BaseViewModel
-import com.google.android.material.snackbar.Snackbar
-import dev.chrisbanes.insetter.applyInsetter
-
-abstract class BaseComposeViewModelFragment : BaseViewModelFragment() {
-
- override fun inflateBinding(
- inflater: LayoutInflater,
- container: ViewGroup?,
- ) = FragmentComposeBinding.inflate(inflater, container, false)
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- binding.composeView.setup { Content() }
- }
-
- override fun snackbar(text: String): Snackbar = requireView().snackbar(text).apply {
- view.applyInsetter {
- type(navigationBars = true) {
- margin()
- }
- }
- }
-
- @Composable
- abstract fun Content()
-}
diff --git a/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/viewmodel/BaseStateViewModel.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/viewmodel/BaseStateViewModel.kt
deleted file mode 100644
index da539adb..00000000
--- a/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/viewmodel/BaseStateViewModel.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.f0x1d.logfox.arch.viewmodel
-
-import android.app.Application
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.update
-
-abstract class BaseStateViewModel(
- initialStateProvider: () -> T,
- application: Application,
-) : BaseViewModel(application) {
-
- private val mutableUiState = MutableStateFlow(initialStateProvider())
- val uiState = mutableUiState.asStateFlow()
- val currentState: T = uiState.value
-
- protected fun state(block: T.() -> T) = mutableUiState.update(block)
-}
diff --git a/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/viewmodel/BaseViewModel.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/viewmodel/BaseViewModel.kt
index 496497b2..f2ec8880 100644
--- a/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/viewmodel/BaseViewModel.kt
+++ b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/viewmodel/BaseViewModel.kt
@@ -4,28 +4,43 @@ import android.app.Application
import android.content.Context
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
-import com.f0x1d.logfox.strings.Strings
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlin.coroutines.CoroutineContext
-abstract class BaseViewModel(
+abstract class BaseViewModel(
+ initialStateProvider: () -> S,
application: Application,
) : AndroidViewModel(application) {
+ val state: StateFlow get() = mutableState.asStateFlow()
+ val actions: Flow get() = actionsChannel.receiveAsFlow()
- private val eventsChannel = Channel(capacity = Channel.BUFFERED)
- val eventsFlow = eventsChannel.receiveAsFlow()
+ val currentState: S get() = mutableState.value
protected val ctx: Context get() = getApplication()
+ private val mutableState = MutableStateFlow(initialStateProvider())
+ private val actionsChannel = Channel(capacity = Channel.UNLIMITED)
+
+ protected fun reduce(block: S.() -> S) = mutableState.update(block)
+
+ protected fun sendAction(action: A) {
+ actionsChannel.trySend(action)
+ }
+
protected fun launchCatching(
context: CoroutineContext = Dispatchers.Main,
- errorBlock: suspend CoroutineScope.() -> Unit = {},
+ errorBlock: suspend CoroutineScope.() -> Unit = { },
block: suspend CoroutineScope.() -> Unit,
) = viewModelScope.launch(context) {
try {
@@ -38,17 +53,6 @@ abstract class BaseViewModel(
errorBlock(this)
e.printStackTrace()
-
- snackbar(ctx.getString(Strings.error, e.localizedMessage))
- }
- }
-
- protected fun snackbar(id: Int) = snackbar(ctx.getString(id))
- protected fun snackbar(text: String) = sendEvent(ShowSnackbar(text))
-
- protected fun sendEvent(event: Event) {
- viewModelScope.launch {
- eventsChannel.send(event)
}
}
}
diff --git a/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/viewmodel/Event.kt b/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/viewmodel/Event.kt
deleted file mode 100644
index 43890554..00000000
--- a/core/arch/src/main/kotlin/com/f0x1d/logfox/arch/viewmodel/Event.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.f0x1d.logfox.arch.viewmodel
-
-interface Event
-
-data class ShowSnackbar(
- val text: String,
-) : Event
diff --git a/core/navigation/src/main/res/navigation/crashes.xml b/core/navigation/src/main/res/navigation/crashes.xml
index 0e44bf32..4b1a537c 100644
--- a/core/navigation/src/main/res/navigation/crashes.xml
+++ b/core/navigation/src/main/res/navigation/crashes.xml
@@ -6,7 +6,7 @@
diff --git a/core/navigation/src/main/res/navigation/logs.xml b/core/navigation/src/main/res/navigation/logs.xml
index 464a692c..b9e15b91 100644
--- a/core/navigation/src/main/res/navigation/logs.xml
+++ b/core/navigation/src/main/res/navigation/logs.xml
@@ -6,7 +6,7 @@
diff --git a/core/navigation/src/main/res/navigation/nav_graph.xml b/core/navigation/src/main/res/navigation/nav_graph.xml
index 7ea2c2de..7feb99de 100644
--- a/core/navigation/src/main/res/navigation/nav_graph.xml
+++ b/core/navigation/src/main/res/navigation/nav_graph.xml
@@ -11,7 +11,7 @@
diff --git a/core/terminals/src/main/aidl/com/f0x1d/logfox/IUserService.aidl b/core/terminals/src/main/aidl/com/f0x1d/logfox/IUserService.aidl
index 3e0b6e52..5edecda9 100644
--- a/core/terminals/src/main/aidl/com/f0x1d/logfox/IUserService.aidl
+++ b/core/terminals/src/main/aidl/com/f0x1d/logfox/IUserService.aidl
@@ -1,6 +1,6 @@
package com.f0x1d.logfox;
-import com.f0x1d.logfox.model.terminal.TerminalResult;
+import com.f0x1d.logfox.models.TerminalResult;
interface IUserService {
void destroy() = 16777114;
@@ -15,4 +15,4 @@ interface IUserService {
ParcelFileDescriptor processInput(long processId) = 6;
void destroyProcess(long processId) = 7;
-}
\ No newline at end of file
+}
diff --git a/core/terminals/src/main/aidl/com/f0x1d/logfox/model/terminal/TerminalResult.aidl b/core/terminals/src/main/aidl/com/f0x1d/logfox/model/terminal/TerminalResult.aidl
deleted file mode 100644
index a1423c76..00000000
--- a/core/terminals/src/main/aidl/com/f0x1d/logfox/model/terminal/TerminalResult.aidl
+++ /dev/null
@@ -1,3 +0,0 @@
-package com.f0x1d.logfox.model.terminal;
-
-parcelable TerminalResult;
\ No newline at end of file
diff --git a/core/terminals/src/main/aidl/com/f0x1d/logfox/models/TerminalResult.aidl b/core/terminals/src/main/aidl/com/f0x1d/logfox/models/TerminalResult.aidl
new file mode 100644
index 00000000..3cebf984
--- /dev/null
+++ b/core/terminals/src/main/aidl/com/f0x1d/logfox/models/TerminalResult.aidl
@@ -0,0 +1,3 @@
+package com.f0x1d.logfox.models;
+
+parcelable TerminalResult;
diff --git a/data/src/main/kotlin/com/f0x1d/logfox/model/terminal/TerminalProcess.kt b/core/terminals/src/main/kotlin/com/f0x1d/logfox/models/TerminalProcess.kt
similarity index 83%
rename from data/src/main/kotlin/com/f0x1d/logfox/model/terminal/TerminalProcess.kt
rename to core/terminals/src/main/kotlin/com/f0x1d/logfox/models/TerminalProcess.kt
index 4b5a6c11..7afce199 100644
--- a/data/src/main/kotlin/com/f0x1d/logfox/model/terminal/TerminalProcess.kt
+++ b/core/terminals/src/main/kotlin/com/f0x1d/logfox/models/TerminalProcess.kt
@@ -1,4 +1,4 @@
-package com.f0x1d.logfox.model.terminal
+package com.f0x1d.logfox.models
import java.io.InputStream
import java.io.OutputStream
diff --git a/data/src/main/kotlin/com/f0x1d/logfox/model/terminal/TerminalResult.kt b/core/terminals/src/main/kotlin/com/f0x1d/logfox/models/TerminalResult.kt
similarity index 86%
rename from data/src/main/kotlin/com/f0x1d/logfox/model/terminal/TerminalResult.kt
rename to core/terminals/src/main/kotlin/com/f0x1d/logfox/models/TerminalResult.kt
index e69aa2c4..ee3409d0 100644
--- a/data/src/main/kotlin/com/f0x1d/logfox/model/terminal/TerminalResult.kt
+++ b/core/terminals/src/main/kotlin/com/f0x1d/logfox/models/TerminalResult.kt
@@ -1,4 +1,4 @@
-package com.f0x1d.logfox.model.terminal
+package com.f0x1d.logfox.models
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@@ -10,4 +10,4 @@ data class TerminalResult(
val errorOutput: String = ""
): Parcelable {
val isSuccessful get() = exitCode == 0
-}
\ No newline at end of file
+}
diff --git a/core/terminals/src/main/kotlin/com/f0x1d/logfox/service/UserService.kt b/core/terminals/src/main/kotlin/com/f0x1d/logfox/service/UserService.kt
index 248995c7..02a48523 100644
--- a/core/terminals/src/main/kotlin/com/f0x1d/logfox/service/UserService.kt
+++ b/core/terminals/src/main/kotlin/com/f0x1d/logfox/service/UserService.kt
@@ -6,7 +6,7 @@ import android.os.ParcelFileDescriptor.AutoCloseInputStream
import android.os.ParcelFileDescriptor.AutoCloseOutputStream
import androidx.annotation.Keep
import com.f0x1d.logfox.IUserService
-import com.f0x1d.logfox.model.terminal.TerminalResult
+import com.f0x1d.logfox.models.TerminalResult
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@@ -20,7 +20,7 @@ import java.io.InputStream
import java.io.OutputStream
import kotlin.system.exitProcess
-class UserService(): IUserService.Stub() {
+class UserService() : IUserService.Stub() {
private val serviceScopeJob = SupervisorJob()
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceScopeJob)
@@ -31,7 +31,7 @@ class UserService(): IUserService.Stub() {
// Needed for shizuku v13
@Suppress("UNUSED_PARAMETER")
@Keep
- constructor(context: Context): this()
+ constructor(context: Context) : this()
override fun destroy() {
serviceScope.cancel()
diff --git a/core/terminals/src/main/kotlin/com/f0x1d/logfox/terminals/DefaultTerminal.kt b/core/terminals/src/main/kotlin/com/f0x1d/logfox/terminals/DefaultTerminal.kt
index b54b319f..bc039c31 100644
--- a/core/terminals/src/main/kotlin/com/f0x1d/logfox/terminals/DefaultTerminal.kt
+++ b/core/terminals/src/main/kotlin/com/f0x1d/logfox/terminals/DefaultTerminal.kt
@@ -1,8 +1,8 @@
package com.f0x1d.logfox.terminals
import com.f0x1d.logfox.arch.di.IODispatcher
-import com.f0x1d.logfox.model.terminal.TerminalProcess
-import com.f0x1d.logfox.model.terminal.TerminalResult
+import com.f0x1d.logfox.models.TerminalProcess
+import com.f0x1d.logfox.models.TerminalResult
import com.f0x1d.logfox.terminals.base.Terminal
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.async
diff --git a/core/terminals/src/main/kotlin/com/f0x1d/logfox/terminals/RootTerminal.kt b/core/terminals/src/main/kotlin/com/f0x1d/logfox/terminals/RootTerminal.kt
index 7be5716b..7b4af7ba 100644
--- a/core/terminals/src/main/kotlin/com/f0x1d/logfox/terminals/RootTerminal.kt
+++ b/core/terminals/src/main/kotlin/com/f0x1d/logfox/terminals/RootTerminal.kt
@@ -1,8 +1,8 @@
package com.f0x1d.logfox.terminals
import com.f0x1d.logfox.arch.di.IODispatcher
-import com.f0x1d.logfox.model.terminal.TerminalProcess
-import com.f0x1d.logfox.model.terminal.TerminalResult
+import com.f0x1d.logfox.models.TerminalProcess
+import com.f0x1d.logfox.models.TerminalResult
import com.f0x1d.logfox.terminals.base.Terminal
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.CoroutineDispatcher
diff --git a/core/terminals/src/main/kotlin/com/f0x1d/logfox/terminals/ShizukuTerminal.kt b/core/terminals/src/main/kotlin/com/f0x1d/logfox/terminals/ShizukuTerminal.kt
index 2d25681a..35d734a7 100644
--- a/core/terminals/src/main/kotlin/com/f0x1d/logfox/terminals/ShizukuTerminal.kt
+++ b/core/terminals/src/main/kotlin/com/f0x1d/logfox/terminals/ShizukuTerminal.kt
@@ -9,8 +9,8 @@ import android.os.ParcelFileDescriptor.AutoCloseInputStream
import android.os.ParcelFileDescriptor.AutoCloseOutputStream
import com.f0x1d.logfox.IUserService
import com.f0x1d.logfox.arch.di.IODispatcher
-import com.f0x1d.logfox.model.terminal.TerminalProcess
-import com.f0x1d.logfox.model.terminal.TerminalResult
+import com.f0x1d.logfox.models.TerminalProcess
+import com.f0x1d.logfox.models.TerminalResult
import com.f0x1d.logfox.service.UserService
import com.f0x1d.logfox.strings.Strings
import com.f0x1d.logfox.terminals.base.Terminal
diff --git a/core/terminals/src/main/kotlin/com/f0x1d/logfox/terminals/base/Terminal.kt b/core/terminals/src/main/kotlin/com/f0x1d/logfox/terminals/base/Terminal.kt
index e3fd00a0..b13b2930 100644
--- a/core/terminals/src/main/kotlin/com/f0x1d/logfox/terminals/base/Terminal.kt
+++ b/core/terminals/src/main/kotlin/com/f0x1d/logfox/terminals/base/Terminal.kt
@@ -1,7 +1,7 @@
package com.f0x1d.logfox.terminals.base
-import com.f0x1d.logfox.model.terminal.TerminalProcess
-import com.f0x1d.logfox.model.terminal.TerminalResult
+import com.f0x1d.logfox.models.TerminalProcess
+import com.f0x1d.logfox.models.TerminalResult
interface Terminal {
val title: Int
diff --git a/feature/apps-picker/api/.gitignore b/feature/apps-picker/api/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/feature/apps-picker/api/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/feature/apps-picker/api/build.gradle.kts b/feature/apps-picker/api/build.gradle.kts
new file mode 100644
index 00000000..b1e5bd81
--- /dev/null
+++ b/feature/apps-picker/api/build.gradle.kts
@@ -0,0 +1,9 @@
+plugins {
+ id("logfox.android.feature")
+}
+
+android.namespace = "com.f0x1d.logfox.feature.apps.picker.api"
+
+dependencies {
+
+}
diff --git a/feature/apps-picker/api/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/AppsPickerResultHandler.kt b/feature/apps-picker/api/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/AppsPickerResultHandler.kt
new file mode 100644
index 00000000..e3926325
--- /dev/null
+++ b/feature/apps-picker/api/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/AppsPickerResultHandler.kt
@@ -0,0 +1,18 @@
+package com.f0x1d.logfox.feature.apps.picker
+
+import android.content.Context
+import com.f0x1d.logfox.strings.Strings
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+
+interface AppsPickerResultHandler {
+ val supportsMultiplySelection: Boolean get() = false
+ val checkedAppPackageNames: Flow> get() = flowOf(emptySet())
+
+ fun providePickerTopAppBarTitle(context: Context) = context.getString(Strings.apps)
+
+ fun onAppChecked(app: InstalledApp, checked: Boolean) = Unit
+
+ // pass true to close fragment
+ fun onAppSelected(app: InstalledApp): Boolean = false
+}
diff --git a/data/src/main/kotlin/com/f0x1d/logfox/model/InstalledApp.kt b/feature/apps-picker/api/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/InstalledApp.kt
similarity index 61%
rename from data/src/main/kotlin/com/f0x1d/logfox/model/InstalledApp.kt
rename to feature/apps-picker/api/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/InstalledApp.kt
index b77e3ea0..69680342 100644
--- a/data/src/main/kotlin/com/f0x1d/logfox/model/InstalledApp.kt
+++ b/feature/apps-picker/api/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/InstalledApp.kt
@@ -1,4 +1,6 @@
-package com.f0x1d.logfox.model
+package com.f0x1d.logfox.feature.apps.picker
+
+import com.f0x1d.logfox.model.Identifiable
data class InstalledApp(
val title: String,
diff --git a/data/.gitignore b/feature/apps-picker/impl/.gitignore
similarity index 100%
rename from data/.gitignore
rename to feature/apps-picker/impl/.gitignore
diff --git a/feature/apps-picker/build.gradle.kts b/feature/apps-picker/impl/build.gradle.kts
similarity index 76%
rename from feature/apps-picker/build.gradle.kts
rename to feature/apps-picker/impl/build.gradle.kts
index e215e003..8037462f 100644
--- a/feature/apps-picker/build.gradle.kts
+++ b/feature/apps-picker/impl/build.gradle.kts
@@ -5,5 +5,7 @@ plugins {
android.namespace = "com.f0x1d.logfox.feature.apps.picker"
dependencies {
+ implementation(projects.feature.appsPicker.api)
+
implementation(libs.coil.compose)
}
diff --git a/feature/apps-picker/impl/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/presentation/AppsPickerAction.kt b/feature/apps-picker/impl/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/presentation/AppsPickerAction.kt
new file mode 100644
index 00000000..a45f1179
--- /dev/null
+++ b/feature/apps-picker/impl/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/presentation/AppsPickerAction.kt
@@ -0,0 +1,3 @@
+package com.f0x1d.logfox.feature.apps.picker.presentation
+
+sealed interface AppsPickerAction
diff --git a/feature/apps-picker/impl/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/presentation/AppsPickerState.kt b/feature/apps-picker/impl/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/presentation/AppsPickerState.kt
new file mode 100644
index 00000000..f82864e7
--- /dev/null
+++ b/feature/apps-picker/impl/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/presentation/AppsPickerState.kt
@@ -0,0 +1,18 @@
+package com.f0x1d.logfox.feature.apps.picker.presentation
+
+import com.f0x1d.logfox.feature.apps.picker.InstalledApp
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.ImmutableSet
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.persistentSetOf
+
+data class AppsPickerState(
+ val topBarTitle: String = "Apps",
+ val apps: ImmutableList = persistentListOf(),
+ val checkedAppPackageNames: ImmutableSet = persistentSetOf(),
+ val searchedApps: ImmutableList = persistentListOf(),
+ val multiplySelectionEnabled: Boolean = true,
+ val isLoading: Boolean = true,
+ val searchActive: Boolean = false,
+ val query: String = "",
+)
diff --git a/feature/apps-picker/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/viewmodel/AppsPickerViewModel.kt b/feature/apps-picker/impl/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/presentation/AppsPickerViewModel.kt
similarity index 63%
rename from feature/apps-picker/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/viewmodel/AppsPickerViewModel.kt
rename to feature/apps-picker/impl/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/presentation/AppsPickerViewModel.kt
index ddbca2a3..f1ea3c64 100644
--- a/feature/apps-picker/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/viewmodel/AppsPickerViewModel.kt
+++ b/feature/apps-picker/impl/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/presentation/AppsPickerViewModel.kt
@@ -1,36 +1,31 @@
-package com.f0x1d.logfox.feature.apps.picker.viewmodel
+package com.f0x1d.logfox.feature.apps.picker.presentation
import android.app.Application
import com.f0x1d.logfox.arch.di.DefaultDispatcher
-import com.f0x1d.logfox.arch.viewmodel.BaseStateViewModel
-import com.f0x1d.logfox.feature.apps.picker.ui.fragment.picker.compose.AppsPickerScreenState
-import com.f0x1d.logfox.model.InstalledApp
+import com.f0x1d.logfox.arch.viewmodel.BaseViewModel
+import com.f0x1d.logfox.feature.apps.picker.InstalledApp
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.update
import javax.inject.Inject
@HiltViewModel
class AppsPickerViewModel @Inject constructor(
@DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher,
application: Application,
-): BaseStateViewModel(
- initialStateProvider = { AppsPickerScreenState() },
+): BaseViewModel(
+ initialStateProvider = { AppsPickerState() },
application = application,
) {
-
- private val query = MutableStateFlow("")
-
init {
load()
}
- fun performBackAction(popBackStack: () -> Unit) = state {
+ fun performBackAction(popBackStack: () -> Unit) = reduce {
if (searchActive) {
copy(searchActive = false)
} else {
@@ -39,14 +34,12 @@ class AppsPickerViewModel @Inject constructor(
}
}
- fun changeSearchActive(active: Boolean) = state {
+ fun changeSearchActive(active: Boolean) = reduce {
copy(searchActive = active)
}
- fun updateQuery(text: String) = state {
- copy(query = text)
- }.also {
- query.update { text }
+ fun updateQuery(query: String) = reduce {
+ copy(query = query)
}
private fun load() = launchCatching(defaultDispatcher) {
@@ -54,19 +47,21 @@ class AppsPickerViewModel @Inject constructor(
val installedApps = packageManager.getInstalledPackages(0).map {
InstalledApp(
- title = it.applicationInfo.loadLabel(packageManager).toString(),
+ title = it.applicationInfo?.loadLabel(packageManager).toString(),
packageName = it.packageName,
)
}.sortedBy(InstalledApp::title)
- state {
+ reduce {
copy(
apps = installedApps.toImmutableList(),
isLoading = false,
)
}
- query.map { query ->
+ state.map { state ->
+ state.query
+ }.distinctUntilChanged().map { query ->
installedApps.filter { app ->
app.title.contains(query, ignoreCase = true)
|| app.packageName.contains(query, ignoreCase = true)
@@ -74,7 +69,7 @@ class AppsPickerViewModel @Inject constructor(
}.flowOn(
defaultDispatcher,
).collectLatest { apps ->
- state { copy(searchedApps = apps.toImmutableList()) }
+ reduce { copy(searchedApps = apps.toImmutableList()) }
}
}
}
diff --git a/feature/apps-picker/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/ui/fragment/picker/AppsPickerFragment.kt b/feature/apps-picker/impl/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/presentation/ui/AppsPickerFragment.kt
similarity index 60%
rename from feature/apps-picker/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/ui/fragment/picker/AppsPickerFragment.kt
rename to feature/apps-picker/impl/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/presentation/ui/AppsPickerFragment.kt
index 2234fe25..0498ef13 100644
--- a/feature/apps-picker/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/ui/fragment/picker/AppsPickerFragment.kt
+++ b/feature/apps-picker/impl/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/presentation/ui/AppsPickerFragment.kt
@@ -1,16 +1,17 @@
-package com.f0x1d.logfox.feature.apps.picker.ui.fragment.picker
+package com.f0x1d.logfox.feature.apps.picker.presentation.ui
+import android.annotation.SuppressLint
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
+import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
-import com.f0x1d.logfox.arch.ui.fragment.compose.BaseComposeViewModelFragment
-import com.f0x1d.logfox.feature.apps.picker.ui.fragment.picker.compose.AppsPickerScreenContent
-import com.f0x1d.logfox.feature.apps.picker.ui.fragment.picker.compose.AppsPickerScreenListener
-import com.f0x1d.logfox.feature.apps.picker.ui.fragment.picker.compose.AppsPickerScreenState
-import com.f0x1d.logfox.feature.apps.picker.viewmodel.AppsPickerViewModel
-import com.f0x1d.logfox.feature.apps.picker.viewmodel.resultHandler
+import com.f0x1d.logfox.arch.presentation.ui.fragment.compose.BaseComposeFragment
+import com.f0x1d.logfox.feature.apps.picker.AppsPickerResultHandler
+import com.f0x1d.logfox.feature.apps.picker.presentation.AppsPickerState
+import com.f0x1d.logfox.feature.apps.picker.presentation.AppsPickerViewModel
+import com.f0x1d.logfox.feature.apps.picker.presentation.ui.compose.AppsPickerScreenContent
import com.f0x1d.logfox.ui.compose.theme.LogFoxTheme
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.collections.immutable.toImmutableSet
@@ -19,9 +20,9 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
@AndroidEntryPoint
-class AppsPickerFragment: BaseComposeViewModelFragment() {
+class AppsPickerFragment : BaseComposeFragment() {
- override val viewModel by viewModels()
+ private val viewModel by viewModels()
private val resultHandler by resultHandler()
private val listener by lazy {
@@ -40,9 +41,9 @@ class AppsPickerFragment: BaseComposeViewModelFragment() {
)
}
- private val uiState: Flow by lazy {
+ private val uiState: Flow by lazy {
resultHandler?.let { handler ->
- combine(viewModel.uiState, handler.checkedAppPackageNames) { state, checkedApps ->
+ combine(viewModel.state, handler.checkedAppPackageNames) { state, checkedApps ->
state to checkedApps
}.map { (state, checkedAppPackageNames) ->
state.copy(
@@ -51,7 +52,7 @@ class AppsPickerFragment: BaseComposeViewModelFragment() {
multiplySelectionEnabled = handler.supportsMultiplySelection,
)
}
- } ?: viewModel.uiState
+ } ?: viewModel.state
}
@Composable
@@ -65,4 +66,16 @@ class AppsPickerFragment: BaseComposeViewModelFragment() {
)
}
}
+
+ @SuppressLint("RestrictedApi")
+ private fun Fragment.resultHandler(): Lazy = lazy {
+ val backStackEntry = findNavController().previousBackStackEntry
+ ?: return@lazy null
+
+ val store = backStackEntry.viewModelStore
+ val availableViewModelKeys = store.keys()
+
+ availableViewModelKeys
+ .firstNotNullOfOrNull { store[it] as? AppsPickerResultHandler }
+ }
}
diff --git a/feature/apps-picker/impl/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/presentation/ui/AppsPickerScreenListener.kt b/feature/apps-picker/impl/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/presentation/ui/AppsPickerScreenListener.kt
new file mode 100644
index 00000000..e12eb9dc
--- /dev/null
+++ b/feature/apps-picker/impl/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/presentation/ui/AppsPickerScreenListener.kt
@@ -0,0 +1,19 @@
+package com.f0x1d.logfox.feature.apps.picker.presentation.ui
+
+import com.f0x1d.logfox.feature.apps.picker.InstalledApp
+
+data class AppsPickerScreenListener(
+ val onBackClicked: () -> Unit,
+ val onAppClicked: (InstalledApp) -> Unit,
+ val onAppChecked: (InstalledApp, Boolean) -> Unit,
+ val onSearchActiveChanged: (Boolean) -> Unit,
+ val onQueryChanged: (String) -> Unit,
+)
+
+internal val MockAppsPickerScreenListener = AppsPickerScreenListener(
+ onBackClicked = { },
+ onAppClicked = { },
+ onAppChecked = { _, _ -> },
+ onSearchActiveChanged = { },
+ onQueryChanged = { },
+)
diff --git a/feature/apps-picker/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/ui/fragment/picker/compose/AppsPickerScreenContent.kt b/feature/apps-picker/impl/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/presentation/ui/compose/AppsPickerScreenContent.kt
similarity index 92%
rename from feature/apps-picker/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/ui/fragment/picker/compose/AppsPickerScreenContent.kt
rename to feature/apps-picker/impl/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/presentation/ui/compose/AppsPickerScreenContent.kt
index 47ce9044..1e3ca551 100644
--- a/feature/apps-picker/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/ui/fragment/picker/compose/AppsPickerScreenContent.kt
+++ b/feature/apps-picker/impl/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/presentation/ui/compose/AppsPickerScreenContent.kt
@@ -1,4 +1,4 @@
-package com.f0x1d.logfox.feature.apps.picker.ui.fragment.picker.compose
+package com.f0x1d.logfox.feature.apps.picker.presentation.ui.compose
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.ExperimentalFoundationApi
@@ -37,7 +37,10 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
-import com.f0x1d.logfox.model.InstalledApp
+import com.f0x1d.logfox.feature.apps.picker.InstalledApp
+import com.f0x1d.logfox.feature.apps.picker.presentation.AppsPickerState
+import com.f0x1d.logfox.feature.apps.picker.presentation.ui.AppsPickerScreenListener
+import com.f0x1d.logfox.feature.apps.picker.presentation.ui.MockAppsPickerScreenListener
import com.f0x1d.logfox.strings.Strings
import com.f0x1d.logfox.ui.compose.component.button.NavigationBackButton
import com.f0x1d.logfox.ui.compose.component.search.TopSearchBar
@@ -50,7 +53,7 @@ import kotlinx.collections.immutable.persistentSetOf
@Composable
internal fun AppsPickerScreenContent(
- state: AppsPickerScreenState = AppsPickerScreenState(),
+ state: AppsPickerState = AppsPickerState(),
listener: AppsPickerScreenListener = MockAppsPickerScreenListener,
) {
CompositionLocalProvider(LocalMultiplySelectionEnabled provides state.multiplySelectionEnabled) {
@@ -83,7 +86,7 @@ internal fun AppsPickerScreenContent(
@Composable
private fun AppsSearchBar(
- state: AppsPickerScreenState,
+ state: AppsPickerState,
listener: AppsPickerScreenListener,
modifier: Modifier = Modifier,
) {
@@ -224,7 +227,7 @@ internal val MockApps = persistentListOf(
InstalledApp("LogFox", "com.f0x1d.logfox"),
InstalledApp("Sense", "com.f0x1d.sense"),
)
-internal val MockAppsPickerScreenState = AppsPickerScreenState(
+internal val MockAppsPickerState = AppsPickerState(
apps = MockApps,
searchedApps = MockApps,
checkedAppPackageNames = persistentSetOf(MockApps.first().packageName),
@@ -235,7 +238,7 @@ internal val MockAppsPickerScreenState = AppsPickerScreenState(
@Composable
private fun AppsPickerScreenContentPreview() = LogFoxTheme {
AppsPickerScreenContent(
- state = MockAppsPickerScreenState,
+ state = MockAppsPickerState,
)
}
@@ -243,7 +246,7 @@ private fun AppsPickerScreenContentPreview() = LogFoxTheme {
@Composable
private fun AppsPickerSearchScreenContentPreview() = LogFoxTheme {
AppsPickerScreenContent(
- state = MockAppsPickerScreenState.copy(searchActive = true),
+ state = MockAppsPickerState.copy(searchActive = true),
)
}
diff --git a/feature/apps-picker/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/ui/fragment/picker/compose/AppsPickerScreenState.kt b/feature/apps-picker/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/ui/fragment/picker/compose/AppsPickerScreenState.kt
deleted file mode 100644
index 71ea5f39..00000000
--- a/feature/apps-picker/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/ui/fragment/picker/compose/AppsPickerScreenState.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-package com.f0x1d.logfox.feature.apps.picker.ui.fragment.picker.compose
-
-import com.f0x1d.logfox.model.InstalledApp
-import kotlinx.collections.immutable.ImmutableList
-import kotlinx.collections.immutable.ImmutableSet
-import kotlinx.collections.immutable.persistentListOf
-import kotlinx.collections.immutable.persistentSetOf
-
-data class AppsPickerScreenState(
- val topBarTitle: String = "Apps",
- val apps: ImmutableList = persistentListOf(),
- val checkedAppPackageNames: ImmutableSet = persistentSetOf(),
- val searchedApps: ImmutableList = persistentListOf(),
- val multiplySelectionEnabled: Boolean = true,
- val isLoading: Boolean = true,
- val searchActive: Boolean = false,
- val query: String = "",
-)
-
-data class AppsPickerScreenListener(
- val onBackClicked: () -> Unit,
- val onAppClicked: (InstalledApp) -> Unit,
- val onAppChecked: (InstalledApp, Boolean) -> Unit,
- val onSearchActiveChanged: (Boolean) -> Unit,
- val onQueryChanged: (String) -> Unit,
-)
-
-internal val MockAppsPickerScreenListener = AppsPickerScreenListener(
- onBackClicked = { },
- onAppClicked = { },
- onAppChecked = { _, _ -> },
- onSearchActiveChanged = { },
- onQueryChanged = { },
-)
diff --git a/feature/apps-picker/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/viewmodel/AppsPickerResultHandler.kt b/feature/apps-picker/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/viewmodel/AppsPickerResultHandler.kt
deleted file mode 100644
index 819b36b3..00000000
--- a/feature/apps-picker/src/main/kotlin/com/f0x1d/logfox/feature/apps/picker/viewmodel/AppsPickerResultHandler.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-package com.f0x1d.logfox.feature.apps.picker.viewmodel
-
-import android.annotation.SuppressLint
-import android.content.Context
-import androidx.fragment.app.Fragment
-import androidx.navigation.fragment.findNavController
-import com.f0x1d.logfox.model.InstalledApp
-import com.f0x1d.logfox.strings.Strings
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.flowOf
-
-interface AppsPickerResultHandler {
- val supportsMultiplySelection: Boolean get() = false
- val checkedAppPackageNames: Flow> get() = flowOf(emptySet())
-
- fun providePickerTopAppBarTitle(context: Context) = context.getString(Strings.apps)
-
- fun onAppChecked(app: InstalledApp, checked: Boolean) = Unit
-
- // pass true to close fragment
- fun onAppSelected(app: InstalledApp): Boolean = false
-}
-
-@SuppressLint("RestrictedApi")
-internal fun Fragment.resultHandler(): Lazy = lazy {
- val backStackEntry = findNavController().previousBackStackEntry
- ?: return@lazy null
-
- val store = backStackEntry.viewModelStore
- val availableViewModelKeys = store.keys()
-
- availableViewModelKeys
- .firstNotNullOfOrNull { store[it] as? AppsPickerResultHandler }
-}
diff --git a/feature/crashes-core/.gitignore b/feature/crashes/api/.gitignore
similarity index 100%
rename from feature/crashes-core/.gitignore
rename to feature/crashes/api/.gitignore
diff --git a/feature/crashes-core/build.gradle.kts b/feature/crashes/api/build.gradle.kts
similarity index 52%
rename from feature/crashes-core/build.gradle.kts
rename to feature/crashes/api/build.gradle.kts
index b36df4d1..c4d94679 100644
--- a/feature/crashes-core/build.gradle.kts
+++ b/feature/crashes/api/build.gradle.kts
@@ -2,7 +2,7 @@ plugins {
id("logfox.android.feature")
}
-android.namespace = "com.f0x1d.logfox.feature.crashes.core"
+android.namespace = "com.f0x1d.logfox.feature.crashes.api"
dependencies {
diff --git a/feature/crashes/api/src/main/kotlin/com/f0x1d/logfox/feature/crashes/api/data/CrashesController.kt b/feature/crashes/api/src/main/kotlin/com/f0x1d/logfox/feature/crashes/api/data/CrashesController.kt
new file mode 100644
index 00000000..b822538e
--- /dev/null
+++ b/feature/crashes/api/src/main/kotlin/com/f0x1d/logfox/feature/crashes/api/data/CrashesController.kt
@@ -0,0 +1,7 @@
+package com.f0x1d.logfox.feature.crashes.api.data
+
+import com.f0x1d.logfox.model.logline.LogLine
+
+interface CrashesController {
+ val readers: List Unit>
+}
diff --git a/feature/crashes/api/src/main/kotlin/com/f0x1d/logfox/feature/crashes/api/data/CrashesNotificationsController.kt b/feature/crashes/api/src/main/kotlin/com/f0x1d/logfox/feature/crashes/api/data/CrashesNotificationsController.kt
new file mode 100644
index 00000000..fb17043b
--- /dev/null
+++ b/feature/crashes/api/src/main/kotlin/com/f0x1d/logfox/feature/crashes/api/data/CrashesNotificationsController.kt
@@ -0,0 +1,12 @@
+package com.f0x1d.logfox.feature.crashes.api.data
+
+import com.f0x1d.logfox.arch.CRASHES_CHANNEL_GROUP_ID
+import com.f0x1d.logfox.database.entity.AppCrash
+
+interface CrashesNotificationsController {
+ fun sendErrorNotification(appCrash: AppCrash, crashLog: String?)
+ fun cancelCrashNotificationFor(appCrash: AppCrash)
+ fun cancelAllCrashNotifications()
+}
+
+val AppCrash.notificationChannelId get() = "${CRASHES_CHANNEL_GROUP_ID}_${crashType.readableName}_$packageName"
diff --git a/feature/crashes/api/src/main/kotlin/com/f0x1d/logfox/feature/crashes/api/data/CrashesRepository.kt b/feature/crashes/api/src/main/kotlin/com/f0x1d/logfox/feature/crashes/api/data/CrashesRepository.kt
new file mode 100644
index 00000000..5ce3cb94
--- /dev/null
+++ b/feature/crashes/api/src/main/kotlin/com/f0x1d/logfox/feature/crashes/api/data/CrashesRepository.kt
@@ -0,0 +1,15 @@
+package com.f0x1d.logfox.feature.crashes.api.data
+
+import com.f0x1d.logfox.arch.repository.DatabaseProxyRepository
+import com.f0x1d.logfox.database.entity.AppCrash
+
+interface CrashesRepository : DatabaseProxyRepository {
+ suspend fun getAllByDateAndTime(
+ dateAndTime: Long,
+ packageName: String,
+ ): List
+
+ suspend fun insert(appCrash: AppCrash): Long
+
+ suspend fun deleteAllByPackageName(appCrash: AppCrash)
+}
diff --git a/feature/crashes/api/src/main/kotlin/com/f0x1d/logfox/feature/crashes/api/data/DisabledAppsRepository.kt b/feature/crashes/api/src/main/kotlin/com/f0x1d/logfox/feature/crashes/api/data/DisabledAppsRepository.kt
new file mode 100644
index 00000000..1c32f953
--- /dev/null
+++ b/feature/crashes/api/src/main/kotlin/com/f0x1d/logfox/feature/crashes/api/data/DisabledAppsRepository.kt
@@ -0,0 +1,13 @@
+package com.f0x1d.logfox.feature.crashes.api.data
+
+import com.f0x1d.logfox.arch.repository.DatabaseProxyRepository
+import com.f0x1d.logfox.database.entity.DisabledApp
+import kotlinx.coroutines.flow.Flow
+
+interface DisabledAppsRepository : DatabaseProxyRepository {
+ suspend fun isDisabledFor(packageName: String): Boolean
+ fun disabledForFlow(packageName: String): Flow
+
+ suspend fun checkApp(packageName: String)
+ suspend fun checkApp(packageName: String, checked: Boolean)
+}
diff --git a/feature/crashes/apps-list/.gitignore b/feature/crashes/apps-list/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/feature/crashes/apps-list/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/feature/crashes/apps-list/build.gradle.kts b/feature/crashes/apps-list/build.gradle.kts
new file mode 100644
index 00000000..6100bef7
--- /dev/null
+++ b/feature/crashes/apps-list/build.gradle.kts
@@ -0,0 +1,10 @@
+plugins {
+ id("logfox.android.feature")
+}
+
+android.namespace = "com.f0x1d.logfox.feature.crashes.apps.list"
+
+dependencies {
+ implementation(projects.feature.crashes.api)
+ implementation(projects.feature.crashes.common)
+}
diff --git a/feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/di/AppCrashesViewModelModule.kt b/feature/crashes/apps-list/src/main/kotlin/com/f0x1d/logfox/feature/crashes/apps/list/di/AppCrashesViewModelModule.kt
similarity index 93%
rename from feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/di/AppCrashesViewModelModule.kt
rename to feature/crashes/apps-list/src/main/kotlin/com/f0x1d/logfox/feature/crashes/apps/list/di/AppCrashesViewModelModule.kt
index 96c750d7..3bb97a47 100644
--- a/feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/di/AppCrashesViewModelModule.kt
+++ b/feature/crashes/apps-list/src/main/kotlin/com/f0x1d/logfox/feature/crashes/apps/list/di/AppCrashesViewModelModule.kt
@@ -1,4 +1,4 @@
-package com.f0x1d.logfox.feature.crashes.di
+package com.f0x1d.logfox.feature.crashes.apps.list.di
import androidx.lifecycle.SavedStateHandle
import dagger.Module
diff --git a/feature/crashes/apps-list/src/main/kotlin/com/f0x1d/logfox/feature/crashes/apps/list/presentation/AppCrashesAction.kt b/feature/crashes/apps-list/src/main/kotlin/com/f0x1d/logfox/feature/crashes/apps/list/presentation/AppCrashesAction.kt
new file mode 100644
index 00000000..85fd05af
--- /dev/null
+++ b/feature/crashes/apps-list/src/main/kotlin/com/f0x1d/logfox/feature/crashes/apps/list/presentation/AppCrashesAction.kt
@@ -0,0 +1,3 @@
+package com.f0x1d.logfox.feature.crashes.apps.list.presentation
+
+sealed interface AppCrashesAction
diff --git a/feature/crashes/apps-list/src/main/kotlin/com/f0x1d/logfox/feature/crashes/apps/list/presentation/AppCrashesState.kt b/feature/crashes/apps-list/src/main/kotlin/com/f0x1d/logfox/feature/crashes/apps/list/presentation/AppCrashesState.kt
new file mode 100644
index 00000000..754a932d
--- /dev/null
+++ b/feature/crashes/apps-list/src/main/kotlin/com/f0x1d/logfox/feature/crashes/apps/list/presentation/AppCrashesState.kt
@@ -0,0 +1,7 @@
+package com.f0x1d.logfox.feature.crashes.apps.list.presentation
+
+import com.f0x1d.logfox.database.entity.AppCrashesCount
+
+data class AppCrashesState(
+ val crashes: List = emptyList(),
+)
diff --git a/feature/crashes/apps-list/src/main/kotlin/com/f0x1d/logfox/feature/crashes/apps/list/presentation/AppCrashesViewModel.kt b/feature/crashes/apps-list/src/main/kotlin/com/f0x1d/logfox/feature/crashes/apps/list/presentation/AppCrashesViewModel.kt
new file mode 100644
index 00000000..3d88f4f5
--- /dev/null
+++ b/feature/crashes/apps-list/src/main/kotlin/com/f0x1d/logfox/feature/crashes/apps/list/presentation/AppCrashesViewModel.kt
@@ -0,0 +1,56 @@
+package com.f0x1d.logfox.feature.crashes.apps.list.presentation
+
+import android.app.Application
+import androidx.lifecycle.viewModelScope
+import com.f0x1d.logfox.arch.di.DefaultDispatcher
+import com.f0x1d.logfox.arch.viewmodel.BaseViewModel
+import com.f0x1d.logfox.database.entity.AppCrash
+import com.f0x1d.logfox.database.entity.AppCrashesCount
+import com.f0x1d.logfox.feature.crashes.api.data.CrashesRepository
+import com.f0x1d.logfox.feature.crashes.apps.list.di.AppName
+import com.f0x1d.logfox.feature.crashes.apps.list.di.PackageName
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class AppCrashesViewModel @Inject constructor(
+ @PackageName val packageName: String,
+ @AppName val appName: String?,
+ private val crashesRepository: CrashesRepository,
+ @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher,
+ application: Application,
+): BaseViewModel(
+ initialStateProvider = { AppCrashesState() },
+ application = application,
+) {
+ init {
+ load()
+ }
+
+ private fun load() {
+ viewModelScope.launch {
+ crashesRepository.getAllAsFlow()
+ .map { crashes ->
+ crashes.filter { crash ->
+ crash.packageName == packageName
+ }.map {
+ AppCrashesCount(it)
+ }
+ }
+ .flowOn(defaultDispatcher)
+ .collect { crashes ->
+ reduce {
+ copy(crashes = crashes)
+ }
+ }
+ }
+ }
+
+ fun deleteCrash(appCrash: AppCrash) = launchCatching {
+ crashesRepository.delete(appCrash)
+ }
+}
diff --git a/feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/ui/fragment/list/AppCrashesFragment.kt b/feature/crashes/apps-list/src/main/kotlin/com/f0x1d/logfox/feature/crashes/apps/list/presentation/ui/AppCrashesFragment.kt
similarity index 78%
rename from feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/ui/fragment/list/AppCrashesFragment.kt
rename to feature/crashes/apps-list/src/main/kotlin/com/f0x1d/logfox/feature/crashes/apps/list/presentation/ui/AppCrashesFragment.kt
index 73682d89..889fffae 100644
--- a/feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/ui/fragment/list/AppCrashesFragment.kt
+++ b/feature/crashes/apps-list/src/main/kotlin/com/f0x1d/logfox/feature/crashes/apps/list/presentation/ui/AppCrashesFragment.kt
@@ -1,4 +1,4 @@
-package com.f0x1d.logfox.feature.crashes.ui.fragment.list
+package com.f0x1d.logfox.feature.crashes.apps.list.presentation.ui
import android.os.Bundle
import android.view.LayoutInflater
@@ -8,10 +8,10 @@ import androidx.core.os.bundleOf
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
-import com.f0x1d.logfox.arch.ui.fragment.BaseViewModelFragment
-import com.f0x1d.logfox.feature.crashes.adapter.CrashesAdapter
-import com.f0x1d.logfox.feature.crashes.databinding.FragmentAppCrashesBinding
-import com.f0x1d.logfox.feature.crashes.viewmodel.list.AppCrashesViewModel
+import com.f0x1d.logfox.arch.presentation.ui.fragment.BaseFragment
+import com.f0x1d.logfox.feature.crashes.apps.list.databinding.FragmentAppCrashesBinding
+import com.f0x1d.logfox.feature.crashes.apps.list.presentation.AppCrashesViewModel
+import com.f0x1d.logfox.feature.crashes.common.presentation.adapter.CrashesAdapter
import com.f0x1d.logfox.navigation.Directions
import com.f0x1d.logfox.ui.density.dpToPx
import com.f0x1d.logfox.ui.dialog.showAreYouSureDeleteDialog
@@ -21,9 +21,9 @@ import dagger.hilt.android.AndroidEntryPoint
import dev.chrisbanes.insetter.applyInsetter
@AndroidEntryPoint
-class AppCrashesFragment: BaseViewModelFragment() {
+class AppCrashesFragment: BaseFragment() {
- override val viewModel by viewModels()
+ private val viewModel by viewModels()
private val adapter = CrashesAdapter(
click = {
@@ -73,8 +73,8 @@ class AppCrashesFragment: BaseViewModelFragment Unit,
diff --git a/feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/ui/viewholder/CrashViewHolder.kt b/feature/crashes/common/src/main/kotlin/com/f0x1d/logfox/feature/crashes/common/presentation/ui/viewholder/CrashViewHolder.kt
similarity index 85%
rename from feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/ui/viewholder/CrashViewHolder.kt
rename to feature/crashes/common/src/main/kotlin/com/f0x1d/logfox/feature/crashes/common/presentation/ui/viewholder/CrashViewHolder.kt
index c1610c0a..38720b12 100644
--- a/feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/ui/viewholder/CrashViewHolder.kt
+++ b/feature/crashes/common/src/main/kotlin/com/f0x1d/logfox/feature/crashes/common/presentation/ui/viewholder/CrashViewHolder.kt
@@ -1,10 +1,10 @@
-package com.f0x1d.logfox.feature.crashes.ui.viewholder
+package com.f0x1d.logfox.feature.crashes.common.presentation.ui.viewholder
import android.annotation.SuppressLint
import com.bumptech.glide.Glide
-import com.f0x1d.logfox.arch.ui.viewholder.BaseViewHolder
+import com.f0x1d.logfox.arch.presentation.ui.viewholder.BaseViewHolder
import com.f0x1d.logfox.database.entity.AppCrashesCount
-import com.f0x1d.logfox.feature.crashes.databinding.ItemCrashBinding
+import com.f0x1d.logfox.feature.crashes.common.databinding.ItemCrashBinding
import com.f0x1d.logfox.strings.Strings
import com.f0x1d.logfox.ui.view.loadIcon
import java.util.Date
diff --git a/feature/crashes/src/main/res/layout/item_crash.xml b/feature/crashes/common/src/main/res/layout/item_crash.xml
similarity index 100%
rename from feature/crashes/src/main/res/layout/item_crash.xml
rename to feature/crashes/common/src/main/res/layout/item_crash.xml
diff --git a/feature/crashes/src/main/res/layout/placeholder_crashes.xml b/feature/crashes/common/src/main/res/layout/placeholder_crashes.xml
similarity index 100%
rename from feature/crashes/src/main/res/layout/placeholder_crashes.xml
rename to feature/crashes/common/src/main/res/layout/placeholder_crashes.xml
diff --git a/feature/crashes/details/.gitignore b/feature/crashes/details/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/feature/crashes/details/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/feature/crashes/details/build.gradle.kts b/feature/crashes/details/build.gradle.kts
new file mode 100644
index 00000000..15b0ea8d
--- /dev/null
+++ b/feature/crashes/details/build.gradle.kts
@@ -0,0 +1,10 @@
+plugins {
+ id("logfox.android.feature")
+}
+
+android.namespace = "com.f0x1d.logfox.feature.crashes.details"
+
+dependencies {
+ implementation(projects.feature.crashes.api)
+ implementation(projects.feature.crashes.common)
+}
diff --git a/feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/di/CrashDetailsViewModelModule.kt b/feature/crashes/details/src/main/kotlin/com/f0x1d/logfox/feature/crashes/details/di/CrashDetailsViewModelModule.kt
similarity index 91%
rename from feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/di/CrashDetailsViewModelModule.kt
rename to feature/crashes/details/src/main/kotlin/com/f0x1d/logfox/feature/crashes/details/di/CrashDetailsViewModelModule.kt
index be96f1bf..932d7d95 100644
--- a/feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/di/CrashDetailsViewModelModule.kt
+++ b/feature/crashes/details/src/main/kotlin/com/f0x1d/logfox/feature/crashes/details/di/CrashDetailsViewModelModule.kt
@@ -1,4 +1,4 @@
-package com.f0x1d.logfox.feature.crashes.di
+package com.f0x1d.logfox.feature.crashes.details.di
import androidx.lifecycle.SavedStateHandle
import dagger.Module
diff --git a/feature/crashes/details/src/main/kotlin/com/f0x1d/logfox/feature/crashes/details/presentation/CrashDetailsAction.kt b/feature/crashes/details/src/main/kotlin/com/f0x1d/logfox/feature/crashes/details/presentation/CrashDetailsAction.kt
new file mode 100644
index 00000000..d0c54ee8
--- /dev/null
+++ b/feature/crashes/details/src/main/kotlin/com/f0x1d/logfox/feature/crashes/details/presentation/CrashDetailsAction.kt
@@ -0,0 +1,3 @@
+package com.f0x1d.logfox.feature.crashes.details.presentation
+
+sealed interface CrashDetailsAction
diff --git a/feature/crashes/details/src/main/kotlin/com/f0x1d/logfox/feature/crashes/details/presentation/CrashDetailsState.kt b/feature/crashes/details/src/main/kotlin/com/f0x1d/logfox/feature/crashes/details/presentation/CrashDetailsState.kt
new file mode 100644
index 00000000..b5c5cfc3
--- /dev/null
+++ b/feature/crashes/details/src/main/kotlin/com/f0x1d/logfox/feature/crashes/details/presentation/CrashDetailsState.kt
@@ -0,0 +1,9 @@
+package com.f0x1d.logfox.feature.crashes.details.presentation
+
+import com.f0x1d.logfox.database.entity.AppCrash
+
+data class CrashDetailsState(
+ val crash: AppCrash? = null,
+ val crashLog: String? = null,
+ val blacklisted: Boolean? = null,
+)
diff --git a/feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/viewmodel/CrashDetailsViewModel.kt b/feature/crashes/details/src/main/kotlin/com/f0x1d/logfox/feature/crashes/details/presentation/CrashDetailsViewModel.kt
similarity index 57%
rename from feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/viewmodel/CrashDetailsViewModel.kt
rename to feature/crashes/details/src/main/kotlin/com/f0x1d/logfox/feature/crashes/details/presentation/CrashDetailsViewModel.kt
index 01ce1dd3..1f005f4d 100644
--- a/feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/viewmodel/CrashDetailsViewModel.kt
+++ b/feature/crashes/details/src/main/kotlin/com/f0x1d/logfox/feature/crashes/details/presentation/CrashDetailsViewModel.kt
@@ -1,4 +1,4 @@
-package com.f0x1d.logfox.feature.crashes.viewmodel
+package com.f0x1d.logfox.feature.crashes.details.presentation
import android.app.Application
import android.net.Uri
@@ -9,19 +9,21 @@ import com.f0x1d.logfox.arch.io.putZipEntry
import com.f0x1d.logfox.arch.viewmodel.BaseViewModel
import com.f0x1d.logfox.database.entity.AppCrash
import com.f0x1d.logfox.datetime.DateTimeFormatter
-import com.f0x1d.logfox.feature.crashes.core.repository.CrashesRepository
-import com.f0x1d.logfox.feature.crashes.core.repository.DisabledAppsRepository
-import com.f0x1d.logfox.feature.crashes.di.CrashId
+import com.f0x1d.logfox.feature.crashes.api.data.CrashesRepository
+import com.f0x1d.logfox.feature.crashes.api.data.DisabledAppsRepository
+import com.f0x1d.logfox.feature.crashes.details.di.CrashId
import com.f0x1d.logfox.model.deviceData
import com.f0x1d.logfox.preferences.shared.AppPreferences
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
@@ -33,42 +35,54 @@ class CrashDetailsViewModel @Inject constructor(
@IODispatcher private val ioDispatcher: CoroutineDispatcher,
dateTimeFormatter: DateTimeFormatter,
application: Application,
-): BaseViewModel(application), DateTimeFormatter by dateTimeFormatter {
-
- val crash = crashesRepository.getByIdAsFlow(crashId)
- .map {
- when (it) {
- null -> null
+): BaseViewModel(
+ initialStateProvider = { CrashDetailsState() },
+ application = application,
+), DateTimeFormatter by dateTimeFormatter {
+ val wrapCrashLogLines get() = appPreferences.wrapCrashLogLines
+ val useSeparateNotificationsChannelsForCrashes get() = appPreferences.useSeparateNotificationsChannelsForCrashes
- else -> runCatching {
- it to it.logFile?.readText()
- }.getOrNull()
- }
- }
- .stateIn(
- scope = viewModelScope,
- started = SharingStarted.Eagerly,
- initialValue = null,
- )
+ init {
+ load()
+ }
@OptIn(ExperimentalCoroutinesApi::class)
- val blacklisted = crashesRepository.getByIdAsFlow(crashId)
- .flatMapLatest { crash ->
- crash?.let {
- disabledAppsRepository.disabledForFlow(it.packageName)
- } ?: flowOf(null)
- }
- .stateIn(
- scope = viewModelScope,
- started = SharingStarted.Eagerly,
- initialValue = null,
- )
+ private fun load() {
+ viewModelScope.launch {
+ crashesRepository.getByIdAsFlow(crashId)
+ .map {
+ when (it) {
+ null -> null
- val wrapCrashLogLines get() = appPreferences.wrapCrashLogLines
- val useSeparateNotificationsChannelsForCrashes get() = appPreferences.useSeparateNotificationsChannelsForCrashes
+ else -> runCatching {
+ it to it.logFile?.readText()
+ }.getOrNull()
+ }
+ }
+ .flowOn(ioDispatcher)
+ .onEach { value ->
+ value?.let { (crash, crashLog) ->
+ reduce { copy(crash = crash, crashLog = crashLog) }
+ }
+ }
+ .launchIn(this)
+
+ crashesRepository.getByIdAsFlow(crashId)
+ .flatMapLatest { crash ->
+ crash?.let {
+ disabledAppsRepository.disabledForFlow(it.packageName)
+ } ?: flowOf(null)
+ }
+ .onEach { blacklisted ->
+ reduce { copy(blacklisted = blacklisted) }
+ }
+ .launchIn(this)
+ }
+ }
fun exportCrashToZip(uri: Uri) = launchCatching(ioDispatcher) {
- val (appCrash, crashLog) = crash.value ?: return@launchCatching
+ val appCrash = currentState.crash ?: return@launchCatching
+ val crashLog = currentState.crashLog
ctx.contentResolver.openOutputStream(uri)?.use {
it.exportToZip {
diff --git a/feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/ui/fragment/CrashDetailsFragment.kt b/feature/crashes/details/src/main/kotlin/com/f0x1d/logfox/feature/crashes/details/presentation/ui/CrashDetailsFragment.kt
similarity index 69%
rename from feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/ui/fragment/CrashDetailsFragment.kt
rename to feature/crashes/details/src/main/kotlin/com/f0x1d/logfox/feature/crashes/details/presentation/ui/CrashDetailsFragment.kt
index a11af621..c18516ab 100644
--- a/feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/ui/fragment/CrashDetailsFragment.kt
+++ b/feature/crashes/details/src/main/kotlin/com/f0x1d/logfox/feature/crashes/details/presentation/ui/CrashDetailsFragment.kt
@@ -1,4 +1,4 @@
-package com.f0x1d.logfox.feature.crashes.ui.fragment
+package com.f0x1d.logfox.feature.crashes.details.presentation.ui
import android.annotation.SuppressLint
import android.content.Intent
@@ -21,13 +21,13 @@ import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import com.f0x1d.logfox.arch.copyText
import com.f0x1d.logfox.arch.notificationsChannelsAvailable
+import com.f0x1d.logfox.arch.presentation.ui.fragment.BaseFragment
import com.f0x1d.logfox.arch.shareIntent
-import com.f0x1d.logfox.arch.ui.fragment.BaseViewModelFragment
import com.f0x1d.logfox.database.entity.AppCrash
-import com.f0x1d.logfox.feature.crashes.R
-import com.f0x1d.logfox.feature.crashes.core.controller.notificationChannelId
-import com.f0x1d.logfox.feature.crashes.databinding.FragmentCrashDetailsBinding
-import com.f0x1d.logfox.feature.crashes.viewmodel.CrashDetailsViewModel
+import com.f0x1d.logfox.feature.crashes.api.data.notificationChannelId
+import com.f0x1d.logfox.feature.crashes.details.R
+import com.f0x1d.logfox.feature.crashes.details.databinding.FragmentCrashDetailsBinding
+import com.f0x1d.logfox.feature.crashes.details.presentation.CrashDetailsViewModel
import com.f0x1d.logfox.strings.Strings
import com.f0x1d.logfox.ui.Colors
import com.f0x1d.logfox.ui.Icons
@@ -41,9 +41,9 @@ import dev.chrisbanes.insetter.applyInsetter
import java.util.Locale
@AndroidEntryPoint
-class CrashDetailsFragment: BaseViewModelFragment() {
+class CrashDetailsFragment : BaseFragment() {
- override val viewModel by viewModels()
+ private val viewModel by viewModels()
private val zipCrashLauncher = registerForActivityResult(
ActivityResultContracts.CreateDocument("application/zip"),
@@ -75,6 +75,42 @@ class CrashDetailsFragment: BaseViewModelFragment
+ Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
+ data = Uri.fromParts("package", appCrash.packageName, null)
+ }.let(::startActivity)
+ }
+ }
+ setClickListenerOn(R.id.notifications_item) {
+ viewModel.currentState.crash?.let { appCrash ->
+ Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply {
+ putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName)
+ putExtra(Settings.EXTRA_CHANNEL_ID, appCrash.notificationChannelId)
+ }.let(::startActivity)
+ }
+ }
+ setClickListenerOn(R.id.blacklist_item) {
+ viewModel.currentState.crash?.let { appCrash ->
+ if (viewModel.currentState.blacklisted == false) {
+ showAreYouSureDialog(
+ title = Strings.blacklist,
+ message = Strings.warning_blacklist,
+ ) {
+ viewModel.changeBlacklist(appCrash)
+ }
+ } else {
+ viewModel.changeBlacklist(appCrash)
+ }
+ }
+ }
+ setClickListenerOn(R.id.delete_item) {
+ showAreYouSureDeleteDialog {
+ viewModel.currentState.crash?.let(viewModel::deleteCrash)
+ findNavController().popBackStack()
+ }
+ }
}
searchItem.setOnActionExpandListener(
object : MenuItem.OnActionExpandListener {
@@ -100,18 +136,34 @@ class CrashDetailsFragment: BaseViewModelFragment
+ shareButton.setOnClickListener {
+ requireContext().shareIntent(viewModel.currentState.crashLog.orEmpty())
+ }
+
+ zipButton.setOnClickListener {
+ viewModel.currentState.crash?.let { appCrash ->
+ val pkg = appCrash.packageName.replace(".", "-")
+ val formattedDate = viewModel.formatForExport(appCrash.dateAndTime)
+
+ zipCrashLauncher.launch("crash-$pkg-$formattedDate.zip")
+ }
+ }
+
+ viewModel.state.collectWithLifecycle { state ->
+ state.crash?.let { setupFor(it to state.crashLog) }
+
toolbar.menu.findItem(R.id.blacklist_item).apply {
- if (blacklisted == null) {
+ if (state.blacklisted == null) {
isVisible = false
} else {
isVisible = true
- setIcon(if (blacklisted) Icons.ic_check_circle else Icons.ic_block)
- setTitle(if (blacklisted) Strings.remove_from_blacklist else Strings.add_to_blacklist)
+ setIcon(if (state.blacklisted) Icons.ic_check_circle else Icons.ic_block)
+ setTitle(if (state.blacklisted) Strings.remove_from_blacklist else Strings.add_to_blacklist)
}
}
}
@@ -121,8 +173,25 @@ class CrashDetailsFragment: BaseViewModelFragment) {
+ val (appCrash, crashLog) = item
+
+ appLogo.loadIcon(appCrash.packageName)
+ appName.text = appCrash.appName ?: getString(Strings.unknown)
+ appPackage.text = appCrash.packageName
+
+ viewModel.wrapCrashLogLines.let { wrap ->
+ logText.isVisible = wrap
+ logTextScrollableContainer.isVisible = wrap.not()
+ }
+
+ logText.text = crashLog
+ logTextScrollable.text = crashLog
+ }
+
private fun FragmentCrashDetailsBinding.searchInLog(text: String) {
- var stackTrace = viewModel.crash.value?.second ?: return
+ var stackTrace = viewModel.currentState.crashLog ?: return
var query = text
val span = stackTrace.toSpannable()
@@ -147,71 +216,6 @@ class CrashDetailsFragment: BaseViewModelFragment) {
- val (appCrash, crashLog) = item
-
- toolbar.menu.apply {
- setClickListenerOn(R.id.info_item) {
- Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
- data = Uri.fromParts("package", appCrash.packageName, null)
- }.let(::startActivity)
- }
- setClickListenerOn(R.id.notifications_item) {
- Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply {
- putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName)
- putExtra(Settings.EXTRA_CHANNEL_ID, appCrash.notificationChannelId)
- }.let(::startActivity)
- }
- setClickListenerOn(R.id.blacklist_item) {
- if (viewModel.blacklisted.value == false) {
- showAreYouSureDialog(
- title = Strings.blacklist,
- message = Strings.warning_blacklist,
- ) {
- viewModel.changeBlacklist(appCrash)
- }
- } else {
- viewModel.changeBlacklist(appCrash)
- }
- }
- setClickListenerOn(R.id.delete_item) {
- showAreYouSureDeleteDialog {
- viewModel.deleteCrash(appCrash)
- findNavController().popBackStack()
- }
- }
- }
-
- appLogo.loadIcon(appCrash.packageName)
- appName.text = appCrash.appName ?: getString(Strings.unknown)
- appPackage.text = appCrash.packageName
-
- copyButton.setOnClickListener {
- requireContext().copyText(crashLog ?: "")
- snackbar(Strings.text_copied)
- }
-
- shareButton.setOnClickListener {
- requireContext().shareIntent(crashLog ?: "")
- }
-
- zipButton.setOnClickListener {
- val pkg = appCrash.packageName.replace(".", "-")
- val formattedDate = viewModel.formatForExport(appCrash.dateAndTime)
-
- zipCrashLauncher.launch("crash-$pkg-$formattedDate.zip")
- }
-
- viewModel.wrapCrashLogLines.let { wrap ->
- logText.isVisible = wrap
- logTextScrollableContainer.isVisible = wrap.not()
- }
-
- logText.text = crashLog
- logTextScrollable.text = crashLog
- }
-
private val FragmentCrashDetailsBinding.searchItem get() =
toolbar.menu.findItem(R.id.search_item)
}
diff --git a/feature/crashes/src/main/res/layout/fragment_crash_details.xml b/feature/crashes/details/src/main/res/layout/fragment_crash_details.xml
similarity index 100%
rename from feature/crashes/src/main/res/layout/fragment_crash_details.xml
rename to feature/crashes/details/src/main/res/layout/fragment_crash_details.xml
diff --git a/feature/crashes/src/main/res/menu/crash_details_menu.xml b/feature/crashes/details/src/main/res/menu/crash_details_menu.xml
similarity index 100%
rename from feature/crashes/src/main/res/menu/crash_details_menu.xml
rename to feature/crashes/details/src/main/res/menu/crash_details_menu.xml
diff --git a/feature/crashes/impl/.gitignore b/feature/crashes/impl/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/feature/crashes/impl/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/feature/crashes/impl/build.gradle.kts b/feature/crashes/impl/build.gradle.kts
new file mode 100644
index 00000000..7033fbad
--- /dev/null
+++ b/feature/crashes/impl/build.gradle.kts
@@ -0,0 +1,9 @@
+plugins {
+ id("logfox.android.feature")
+}
+
+android.namespace = "com.f0x1d.logfox.feature.crashes.impl"
+
+dependencies {
+ implementation(projects.feature.crashes.api)
+}
diff --git a/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/controller/CrashesController.kt b/feature/crashes/impl/src/main/kotlin/com/f0x1d/logfox/feature/crashes/impl/data/CrashesControllerImpl.kt
similarity index 83%
rename from feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/controller/CrashesController.kt
rename to feature/crashes/impl/src/main/kotlin/com/f0x1d/logfox/feature/crashes/impl/data/CrashesControllerImpl.kt
index 4d7b90d1..deb2c4f1 100644
--- a/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/controller/CrashesController.kt
+++ b/feature/crashes/impl/src/main/kotlin/com/f0x1d/logfox/feature/crashes/impl/data/CrashesControllerImpl.kt
@@ -1,13 +1,15 @@
-package com.f0x1d.logfox.feature.crashes.core.controller
+package com.f0x1d.logfox.feature.crashes.impl.data
import android.content.Context
import com.f0x1d.logfox.arch.di.IODispatcher
import com.f0x1d.logfox.database.entity.AppCrash
-import com.f0x1d.logfox.feature.crashes.core.repository.CrashesRepository
-import com.f0x1d.logfox.feature.crashes.core.repository.DisabledAppsRepository
-import com.f0x1d.logfox.feature.crashes.core.repository.reader.ANRDetector
-import com.f0x1d.logfox.feature.crashes.core.repository.reader.JNICrashDetector
-import com.f0x1d.logfox.feature.crashes.core.repository.reader.JavaCrashDetector
+import com.f0x1d.logfox.feature.crashes.api.data.CrashesController
+import com.f0x1d.logfox.feature.crashes.api.data.CrashesNotificationsController
+import com.f0x1d.logfox.feature.crashes.api.data.CrashesRepository
+import com.f0x1d.logfox.feature.crashes.api.data.DisabledAppsRepository
+import com.f0x1d.logfox.feature.crashes.impl.data.reader.ANRDetector
+import com.f0x1d.logfox.feature.crashes.impl.data.reader.JNICrashDetector
+import com.f0x1d.logfox.feature.crashes.impl.data.reader.JavaCrashDetector
import com.f0x1d.logfox.model.logline.LogLine
import com.f0x1d.logfox.preferences.shared.AppPreferences
import dagger.hilt.android.qualifiers.ApplicationContext
@@ -16,10 +18,6 @@ import kotlinx.coroutines.withContext
import java.io.File
import javax.inject.Inject
-interface CrashesController {
- val readers: List Unit>
-}
-
internal class CrashesControllerImpl @Inject constructor(
@ApplicationContext private val context: Context,
private val notificationsController: CrashesNotificationsController,
diff --git a/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/controller/CrashesNotificationsController.kt b/feature/crashes/impl/src/main/kotlin/com/f0x1d/logfox/feature/crashes/impl/data/CrashesNotificationsControllerImpl.kt
similarity index 93%
rename from feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/controller/CrashesNotificationsController.kt
rename to feature/crashes/impl/src/main/kotlin/com/f0x1d/logfox/feature/crashes/impl/data/CrashesNotificationsControllerImpl.kt
index bf01a755..3428644c 100644
--- a/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/controller/CrashesNotificationsController.kt
+++ b/feature/crashes/impl/src/main/kotlin/com/f0x1d/logfox/feature/crashes/impl/data/CrashesNotificationsControllerImpl.kt
@@ -1,4 +1,4 @@
-package com.f0x1d.logfox.feature.crashes.core.controller
+package com.f0x1d.logfox.feature.crashes.impl.data
import android.annotation.SuppressLint
import android.content.Context
@@ -17,6 +17,8 @@ import com.f0x1d.logfox.arch.notificationManager
import com.f0x1d.logfox.arch.notificationManagerCompat
import com.f0x1d.logfox.arch.receiver.CopyReceiver
import com.f0x1d.logfox.database.entity.AppCrash
+import com.f0x1d.logfox.feature.crashes.api.data.CrashesNotificationsController
+import com.f0x1d.logfox.feature.crashes.api.data.notificationChannelId
import com.f0x1d.logfox.navigation.Directions
import com.f0x1d.logfox.navigation.NavGraphs
import com.f0x1d.logfox.preferences.shared.AppPreferences
@@ -25,12 +27,6 @@ import com.f0x1d.logfox.ui.Icons
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
-internal interface CrashesNotificationsController {
- fun sendErrorNotification(appCrash: AppCrash, crashLog: String?)
- fun cancelCrashNotificationFor(appCrash: AppCrash)
- fun cancelAllCrashNotifications()
-}
-
@SuppressLint("MissingPermission")
internal class CrashesNotificationsControllerImpl @Inject constructor(
@ApplicationContext private val context: Context,
@@ -144,5 +140,3 @@ internal class CrashesNotificationsControllerImpl @Inject constructor(
private const val CRASHES_CHANNEL_ID = "crashes"
}
}
-
-val AppCrash.notificationChannelId get() = "${CRASHES_CHANNEL_GROUP_ID}_${crashType.readableName}_$packageName"
diff --git a/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/CrashesRepository.kt b/feature/crashes/impl/src/main/kotlin/com/f0x1d/logfox/feature/crashes/impl/data/CrashesRepositoryImpl.kt
similarity index 83%
rename from feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/CrashesRepository.kt
rename to feature/crashes/impl/src/main/kotlin/com/f0x1d/logfox/feature/crashes/impl/data/CrashesRepositoryImpl.kt
index e8119600..840a8547 100644
--- a/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/CrashesRepository.kt
+++ b/feature/crashes/impl/src/main/kotlin/com/f0x1d/logfox/feature/crashes/impl/data/CrashesRepositoryImpl.kt
@@ -1,10 +1,10 @@
-package com.f0x1d.logfox.feature.crashes.core.repository
+package com.f0x1d.logfox.feature.crashes.impl.data
import com.f0x1d.logfox.arch.di.IODispatcher
-import com.f0x1d.logfox.arch.repository.DatabaseProxyRepository
import com.f0x1d.logfox.database.AppDatabase
import com.f0x1d.logfox.database.entity.AppCrash
-import com.f0x1d.logfox.feature.crashes.core.controller.CrashesNotificationsController
+import com.f0x1d.logfox.feature.crashes.api.data.CrashesNotificationsController
+import com.f0x1d.logfox.feature.crashes.api.data.CrashesRepository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -12,16 +12,6 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.withContext
import javax.inject.Inject
-interface CrashesRepository : DatabaseProxyRepository {
- suspend fun getAllByDateAndTime(
- dateAndTime: Long,
- packageName: String,
- ): List
- suspend fun insert(appCrash: AppCrash): Long
-
- suspend fun deleteAllByPackageName(appCrash: AppCrash)
-}
-
internal class CrashesRepositoryImpl @Inject constructor(
private val notificationsController: CrashesNotificationsController,
private val database: AppDatabase,
diff --git a/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/DisabledAppsRepository.kt b/feature/crashes/impl/src/main/kotlin/com/f0x1d/logfox/feature/crashes/impl/data/DisabledAppsRepositoryImpl.kt
similarity index 85%
rename from feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/DisabledAppsRepository.kt
rename to feature/crashes/impl/src/main/kotlin/com/f0x1d/logfox/feature/crashes/impl/data/DisabledAppsRepositoryImpl.kt
index 2bf10270..101bed3a 100644
--- a/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/DisabledAppsRepository.kt
+++ b/feature/crashes/impl/src/main/kotlin/com/f0x1d/logfox/feature/crashes/impl/data/DisabledAppsRepositoryImpl.kt
@@ -1,9 +1,9 @@
-package com.f0x1d.logfox.feature.crashes.core.repository
+package com.f0x1d.logfox.feature.crashes.impl.data
import com.f0x1d.logfox.arch.di.IODispatcher
-import com.f0x1d.logfox.arch.repository.DatabaseProxyRepository
import com.f0x1d.logfox.database.AppDatabase
import com.f0x1d.logfox.database.entity.DisabledApp
+import com.f0x1d.logfox.feature.crashes.api.data.DisabledAppsRepository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -12,14 +12,6 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import javax.inject.Inject
-interface DisabledAppsRepository : DatabaseProxyRepository {
- suspend fun isDisabledFor(packageName: String): Boolean
- fun disabledForFlow(packageName: String): Flow
-
- suspend fun checkApp(packageName: String)
- suspend fun checkApp(packageName: String, checked: Boolean)
-}
-
internal class DisabledAppsRepositoryImpl @Inject constructor(
private val database: AppDatabase,
@IODispatcher private val ioDispatcher: CoroutineDispatcher,
diff --git a/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/ANRDetector.kt b/feature/crashes/impl/src/main/kotlin/com/f0x1d/logfox/feature/crashes/impl/data/reader/ANRDetector.kt
similarity index 84%
rename from feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/ANRDetector.kt
rename to feature/crashes/impl/src/main/kotlin/com/f0x1d/logfox/feature/crashes/impl/data/reader/ANRDetector.kt
index b0bf57c9..ef4f8389 100644
--- a/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/ANRDetector.kt
+++ b/feature/crashes/impl/src/main/kotlin/com/f0x1d/logfox/feature/crashes/impl/data/reader/ANRDetector.kt
@@ -1,9 +1,9 @@
-package com.f0x1d.logfox.feature.crashes.core.repository.reader
+package com.f0x1d.logfox.feature.crashes.impl.data.reader
import android.content.Context
import com.f0x1d.logfox.database.entity.AppCrash
import com.f0x1d.logfox.database.entity.CrashType
-import com.f0x1d.logfox.feature.crashes.core.repository.reader.base.BaseCrashDetector
+import com.f0x1d.logfox.feature.crashes.impl.data.reader.base.BaseCrashDetector
import com.f0x1d.logfox.model.logline.LogLine
internal class ANRDetector(
diff --git a/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/JNICrashDetector.kt b/feature/crashes/impl/src/main/kotlin/com/f0x1d/logfox/feature/crashes/impl/data/reader/JNICrashDetector.kt
similarity index 92%
rename from feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/JNICrashDetector.kt
rename to feature/crashes/impl/src/main/kotlin/com/f0x1d/logfox/feature/crashes/impl/data/reader/JNICrashDetector.kt
index 1f3464b1..52e6c520 100644
--- a/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/JNICrashDetector.kt
+++ b/feature/crashes/impl/src/main/kotlin/com/f0x1d/logfox/feature/crashes/impl/data/reader/JNICrashDetector.kt
@@ -1,9 +1,9 @@
-package com.f0x1d.logfox.feature.crashes.core.repository.reader
+package com.f0x1d.logfox.feature.crashes.impl.data.reader
import android.content.Context
import com.f0x1d.logfox.database.entity.AppCrash
import com.f0x1d.logfox.database.entity.CrashType
-import com.f0x1d.logfox.feature.crashes.core.repository.reader.base.BaseCrashDetector
+import com.f0x1d.logfox.feature.crashes.impl.data.reader.base.BaseCrashDetector
import com.f0x1d.logfox.model.logline.LogLine
import com.f0x1d.logfox.preferences.shared.appPreferences
diff --git a/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/JavaCrashDetector.kt b/feature/crashes/impl/src/main/kotlin/com/f0x1d/logfox/feature/crashes/impl/data/reader/JavaCrashDetector.kt
similarity index 82%
rename from feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/JavaCrashDetector.kt
rename to feature/crashes/impl/src/main/kotlin/com/f0x1d/logfox/feature/crashes/impl/data/reader/JavaCrashDetector.kt
index 563fc52d..f5b9c57e 100644
--- a/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/JavaCrashDetector.kt
+++ b/feature/crashes/impl/src/main/kotlin/com/f0x1d/logfox/feature/crashes/impl/data/reader/JavaCrashDetector.kt
@@ -1,9 +1,9 @@
-package com.f0x1d.logfox.feature.crashes.core.repository.reader
+package com.f0x1d.logfox.feature.crashes.impl.data.reader
import android.content.Context
import com.f0x1d.logfox.database.entity.AppCrash
import com.f0x1d.logfox.database.entity.CrashType
-import com.f0x1d.logfox.feature.crashes.core.repository.reader.base.BaseCrashDetector
+import com.f0x1d.logfox.feature.crashes.impl.data.reader.base.BaseCrashDetector
import com.f0x1d.logfox.model.logline.LogLine
internal class JavaCrashDetector(
diff --git a/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/base/BaseCrashDetector.kt b/feature/crashes/impl/src/main/kotlin/com/f0x1d/logfox/feature/crashes/impl/data/reader/base/BaseCrashDetector.kt
similarity index 96%
rename from feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/base/BaseCrashDetector.kt
rename to feature/crashes/impl/src/main/kotlin/com/f0x1d/logfox/feature/crashes/impl/data/reader/base/BaseCrashDetector.kt
index d8967d58..74c0dd1d 100644
--- a/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/base/BaseCrashDetector.kt
+++ b/feature/crashes/impl/src/main/kotlin/com/f0x1d/logfox/feature/crashes/impl/data/reader/base/BaseCrashDetector.kt
@@ -1,4 +1,4 @@
-package com.f0x1d.logfox.feature.crashes.core.repository.reader.base
+package com.f0x1d.logfox.feature.crashes.impl.data.reader.base
import android.content.Context
import com.f0x1d.logfox.database.entity.AppCrash
@@ -69,7 +69,7 @@ internal abstract class BaseCrashDetector(
lines: List,
) = context.run {
val appName = try {
- packageManager.getPackageInfo(crashedAppPackageName, 0).applicationInfo.let {
+ packageManager.getPackageInfo(crashedAppPackageName, 0).applicationInfo?.let {
packageManager.getApplicationLabel(it).toString()
}
} catch (e: Exception) {
diff --git a/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/base/DefaultChecker.kt b/feature/crashes/impl/src/main/kotlin/com/f0x1d/logfox/feature/crashes/impl/data/reader/base/DefaultChecker.kt
similarity index 85%
rename from feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/base/DefaultChecker.kt
rename to feature/crashes/impl/src/main/kotlin/com/f0x1d/logfox/feature/crashes/impl/data/reader/base/DefaultChecker.kt
index b5544078..67afceb9 100644
--- a/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/repository/reader/base/DefaultChecker.kt
+++ b/feature/crashes/impl/src/main/kotlin/com/f0x1d/logfox/feature/crashes/impl/data/reader/base/DefaultChecker.kt
@@ -1,4 +1,4 @@
-package com.f0x1d.logfox.feature.crashes.core.repository.reader.base
+package com.f0x1d.logfox.feature.crashes.impl.data.reader.base
import com.f0x1d.logfox.model.logline.LogLine
diff --git a/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/di/ControllersModule.kt b/feature/crashes/impl/src/main/kotlin/com/f0x1d/logfox/feature/crashes/impl/di/ControllersModule.kt
similarity index 57%
rename from feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/di/ControllersModule.kt
rename to feature/crashes/impl/src/main/kotlin/com/f0x1d/logfox/feature/crashes/impl/di/ControllersModule.kt
index 0fb72389..e042eaed 100644
--- a/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/di/ControllersModule.kt
+++ b/feature/crashes/impl/src/main/kotlin/com/f0x1d/logfox/feature/crashes/impl/di/ControllersModule.kt
@@ -1,9 +1,9 @@
-package com.f0x1d.logfox.feature.crashes.core.di
+package com.f0x1d.logfox.feature.crashes.impl.di
-import com.f0x1d.logfox.feature.crashes.core.controller.CrashesController
-import com.f0x1d.logfox.feature.crashes.core.controller.CrashesControllerImpl
-import com.f0x1d.logfox.feature.crashes.core.controller.CrashesNotificationsController
-import com.f0x1d.logfox.feature.crashes.core.controller.CrashesNotificationsControllerImpl
+import com.f0x1d.logfox.feature.crashes.api.data.CrashesController
+import com.f0x1d.logfox.feature.crashes.api.data.CrashesNotificationsController
+import com.f0x1d.logfox.feature.crashes.impl.data.CrashesControllerImpl
+import com.f0x1d.logfox.feature.crashes.impl.data.CrashesNotificationsControllerImpl
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
diff --git a/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/di/RepositoriesModule.kt b/feature/crashes/impl/src/main/kotlin/com/f0x1d/logfox/feature/crashes/impl/di/RepositoriesModule.kt
similarity index 56%
rename from feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/di/RepositoriesModule.kt
rename to feature/crashes/impl/src/main/kotlin/com/f0x1d/logfox/feature/crashes/impl/di/RepositoriesModule.kt
index 7249bba7..d6816df4 100644
--- a/feature/crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/di/RepositoriesModule.kt
+++ b/feature/crashes/impl/src/main/kotlin/com/f0x1d/logfox/feature/crashes/impl/di/RepositoriesModule.kt
@@ -1,9 +1,9 @@
-package com.f0x1d.logfox.feature.crashes.core.di
+package com.f0x1d.logfox.feature.crashes.impl.di
-import com.f0x1d.logfox.feature.crashes.core.repository.CrashesRepository
-import com.f0x1d.logfox.feature.crashes.core.repository.CrashesRepositoryImpl
-import com.f0x1d.logfox.feature.crashes.core.repository.DisabledAppsRepository
-import com.f0x1d.logfox.feature.crashes.core.repository.DisabledAppsRepositoryImpl
+import com.f0x1d.logfox.feature.crashes.api.data.CrashesRepository
+import com.f0x1d.logfox.feature.crashes.api.data.DisabledAppsRepository
+import com.f0x1d.logfox.feature.crashes.impl.data.CrashesRepositoryImpl
+import com.f0x1d.logfox.feature.crashes.impl.data.DisabledAppsRepositoryImpl
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
diff --git a/feature/crashes/list/.gitignore b/feature/crashes/list/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/feature/crashes/list/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/feature/crashes/list/build.gradle.kts b/feature/crashes/list/build.gradle.kts
new file mode 100644
index 00000000..2d464dba
--- /dev/null
+++ b/feature/crashes/list/build.gradle.kts
@@ -0,0 +1,12 @@
+plugins {
+ id("logfox.android.feature")
+}
+
+android.namespace = "com.f0x1d.logfox.feature.crashes.list"
+
+dependencies {
+ implementation(projects.feature.crashes.api)
+ implementation(projects.feature.crashes.common)
+
+ implementation(projects.feature.appsPicker.api)
+}
diff --git a/feature/crashes/list/src/main/kotlin/com/f0x1d/logfox/feature/crashes/list/presentation/CrashesAction.kt b/feature/crashes/list/src/main/kotlin/com/f0x1d/logfox/feature/crashes/list/presentation/CrashesAction.kt
new file mode 100644
index 00000000..67f33d25
--- /dev/null
+++ b/feature/crashes/list/src/main/kotlin/com/f0x1d/logfox/feature/crashes/list/presentation/CrashesAction.kt
@@ -0,0 +1,3 @@
+package com.f0x1d.logfox.feature.crashes.list.presentation
+
+sealed interface CrashesAction
diff --git a/feature/crashes/list/src/main/kotlin/com/f0x1d/logfox/feature/crashes/list/presentation/CrashesState.kt b/feature/crashes/list/src/main/kotlin/com/f0x1d/logfox/feature/crashes/list/presentation/CrashesState.kt
new file mode 100644
index 00000000..f4c0b7fc
--- /dev/null
+++ b/feature/crashes/list/src/main/kotlin/com/f0x1d/logfox/feature/crashes/list/presentation/CrashesState.kt
@@ -0,0 +1,12 @@
+package com.f0x1d.logfox.feature.crashes.list.presentation
+
+import com.f0x1d.logfox.database.entity.AppCrashesCount
+import com.f0x1d.logfox.preferences.shared.crashes.CrashesSort
+
+data class CrashesState(
+ val crashes: List = emptyList(),
+ val searchedCrashes: List = emptyList(),
+ val currentSort: CrashesSort = CrashesSort.NEW,
+ val sortInReversedOrder: Boolean = false,
+ val query: String? = null,
+)
diff --git a/feature/crashes/list/src/main/kotlin/com/f0x1d/logfox/feature/crashes/list/presentation/CrashesViewModel.kt b/feature/crashes/list/src/main/kotlin/com/f0x1d/logfox/feature/crashes/list/presentation/CrashesViewModel.kt
new file mode 100644
index 00000000..cdffcce5
--- /dev/null
+++ b/feature/crashes/list/src/main/kotlin/com/f0x1d/logfox/feature/crashes/list/presentation/CrashesViewModel.kt
@@ -0,0 +1,154 @@
+package com.f0x1d.logfox.feature.crashes.list.presentation
+
+import android.app.Application
+import android.content.Context
+import androidx.lifecycle.viewModelScope
+import com.f0x1d.logfox.arch.di.DefaultDispatcher
+import com.f0x1d.logfox.arch.viewmodel.BaseViewModel
+import com.f0x1d.logfox.database.entity.AppCrash
+import com.f0x1d.logfox.database.entity.AppCrashesCount
+import com.f0x1d.logfox.database.entity.DisabledApp
+import com.f0x1d.logfox.feature.apps.picker.AppsPickerResultHandler
+import com.f0x1d.logfox.feature.apps.picker.InstalledApp
+import com.f0x1d.logfox.feature.crashes.api.data.CrashesRepository
+import com.f0x1d.logfox.feature.crashes.api.data.DisabledAppsRepository
+import com.f0x1d.logfox.preferences.shared.AppPreferences
+import com.f0x1d.logfox.preferences.shared.crashes.CrashesSort
+import com.f0x1d.logfox.strings.Strings
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class CrashesViewModel @Inject constructor(
+ private val crashesRepository: CrashesRepository,
+ private val disabledAppsRepository: DisabledAppsRepository,
+ private val appPreferences: AppPreferences,
+ @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher,
+ application: Application,
+): BaseViewModel(
+ initialStateProvider = { CrashesState() },
+ application = application,
+), AppsPickerResultHandler {
+ init {
+ load()
+ }
+
+ private fun load() {
+ viewModelScope.launch {
+ combine(
+ crashesRepository.getAllAsFlow(),
+ appPreferences.crashesSortType.asFlow(),
+ appPreferences.crashesSortReversedOrder.asFlow(),
+ ) { crashes, sortType, sortInReversedOrder ->
+ val groupedCrashes = crashes.groupBy { it.packageName }
+
+ val appCrashes = groupedCrashes.map {
+ AppCrashesCount(
+ lastCrash = it.value.first(),
+ count = it.value.size
+ )
+ }.let(sortType.sorter).let { result ->
+ if (sortInReversedOrder) {
+ result.asReversed()
+ } else {
+ result
+ }
+ }
+
+ CrashesWithSort(
+ crashes = appCrashes,
+ sortType = sortType,
+ sortInReversedOrder = sortInReversedOrder,
+ )
+ }
+ .distinctUntilChanged()
+ .flowOn(defaultDispatcher)
+ .onEach { data ->
+ reduce {
+ copy(
+ crashes = data.crashes,
+ currentSort = data.sortType,
+ sortInReversedOrder = data.sortInReversedOrder,
+ )
+ }
+ }
+ .launchIn(this)
+
+ combine(
+ crashesRepository.getAllAsFlow().distinctUntilChanged(),
+ state.map { it.query.orEmpty() },
+ ) { crashes, query -> crashes to query }
+ .map { (crashes, query) ->
+ crashes.filter { crash ->
+ crash.packageName.contains(query, ignoreCase = true)
+ || crash.appName?.contains(query, ignoreCase = true) == true
+ }.map { AppCrashesCount(it) }
+ }
+ .distinctUntilChanged()
+ .flowOn(defaultDispatcher)
+ .onEach { searchedCrashes ->
+ reduce { copy(searchedCrashes = searchedCrashes) }
+ }
+ .launchIn(this)
+ }
+ }
+
+ fun updateQuery(query: String) = reduce {
+ copy(query = query)
+ }
+
+ fun updateSort(sortType: CrashesSort, sortInReversedOrder: Boolean) = appPreferences.updateCrashesSortSettings(
+ sortType = sortType,
+ sortInReversedOrder = sortInReversedOrder,
+ )
+
+ fun deleteCrashesByPackageName(appCrash: AppCrash) = launchCatching {
+ crashesRepository.deleteAllByPackageName(appCrash)
+ }
+
+ fun deleteCrash(appCrash: AppCrash) = launchCatching {
+ crashesRepository.delete(appCrash)
+ }
+
+ fun clearCrashes() = launchCatching {
+ crashesRepository.clear()
+ }
+
+ override val supportsMultiplySelection: Boolean = true
+
+ override val checkedAppPackageNames: Flow> =
+ disabledAppsRepository.getAllAsFlow().map { apps ->
+ apps.map(DisabledApp::packageName).toSet()
+ }
+
+ override fun providePickerTopAppBarTitle(context: Context): String =
+ context.getString(Strings.blacklist)
+
+ override fun onAppChecked(app: InstalledApp, checked: Boolean) {
+ viewModelScope.launch {
+ disabledAppsRepository.checkApp(app.packageName, checked)
+ }
+ }
+
+ override fun onAppSelected(app: InstalledApp): Boolean {
+ viewModelScope.launch {
+ disabledAppsRepository.checkApp(app.packageName)
+ }
+ return false
+ }
+
+ private data class CrashesWithSort(
+ val crashes: List,
+ val sortType: CrashesSort,
+ val sortInReversedOrder: Boolean,
+ )
+}
diff --git a/feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/ui/fragment/list/CrashesFragment.kt b/feature/crashes/list/src/main/kotlin/com/f0x1d/logfox/feature/crashes/list/presentation/ui/CrashesFragment.kt
similarity index 84%
rename from feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/ui/fragment/list/CrashesFragment.kt
rename to feature/crashes/list/src/main/kotlin/com/f0x1d/logfox/feature/crashes/list/presentation/ui/CrashesFragment.kt
index d2e102ba..83b522b9 100644
--- a/feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/ui/fragment/list/CrashesFragment.kt
+++ b/feature/crashes/list/src/main/kotlin/com/f0x1d/logfox/feature/crashes/list/presentation/ui/CrashesFragment.kt
@@ -1,4 +1,4 @@
-package com.f0x1d.logfox.feature.crashes.ui.fragment.list
+package com.f0x1d.logfox.feature.crashes.list.presentation.ui
import android.os.Bundle
import android.view.LayoutInflater
@@ -12,13 +12,13 @@ import androidx.hilt.navigation.fragment.hiltNavGraphViewModels
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.f0x1d.logfox.arch.isHorizontalOrientation
-import com.f0x1d.logfox.arch.ui.fragment.BaseViewModelFragment
-import com.f0x1d.logfox.feature.crashes.R
-import com.f0x1d.logfox.feature.crashes.adapter.CrashesAdapter
-import com.f0x1d.logfox.feature.crashes.databinding.DialogSortingBinding
-import com.f0x1d.logfox.feature.crashes.databinding.FragmentCrashesBinding
-import com.f0x1d.logfox.feature.crashes.databinding.ItemSortBinding
-import com.f0x1d.logfox.feature.crashes.viewmodel.list.CrashesViewModel
+import com.f0x1d.logfox.arch.presentation.ui.fragment.BaseFragment
+import com.f0x1d.logfox.feature.crashes.common.presentation.adapter.CrashesAdapter
+import com.f0x1d.logfox.feature.crashes.list.R
+import com.f0x1d.logfox.feature.crashes.list.databinding.DialogSortingBinding
+import com.f0x1d.logfox.feature.crashes.list.databinding.FragmentCrashesBinding
+import com.f0x1d.logfox.feature.crashes.list.databinding.ItemSortBinding
+import com.f0x1d.logfox.feature.crashes.list.presentation.CrashesViewModel
import com.f0x1d.logfox.navigation.Directions
import com.f0x1d.logfox.preferences.shared.crashes.CrashesSort
import com.f0x1d.logfox.strings.Strings
@@ -33,9 +33,9 @@ import dagger.hilt.android.AndroidEntryPoint
import dev.chrisbanes.insetter.applyInsetter
@AndroidEntryPoint
-class CrashesFragment: BaseViewModelFragment() {
+class CrashesFragment : BaseFragment() {
- override val viewModel by hiltNavGraphViewModels(Directions.crashesFragment)
+ private val viewModel by hiltNavGraphViewModels(Directions.crashesFragment)
private val adapter = CrashesAdapter(
click = {
@@ -146,13 +146,11 @@ class CrashesFragment: BaseViewModelFragment
+ binding.placeholderLayout.root.isVisible = state.crashes.isEmpty()
- adapter.submitList(it)
- }
- viewModel.searchedCrashes.collectWithLifecycle {
- searchedAdapter.submitList(it)
+ adapter.submitList(state.crashes)
+ searchedAdapter.submitList(state.searchedCrashes)
}
requireActivity().onBackPressedDispatcher.apply {
@@ -161,8 +159,8 @@ class CrashesFragment: BaseViewModelFragment
@@ -181,7 +179,7 @@ class CrashesFragment: BaseViewModelFragment
- crashes.filter { crash ->
- crash.packageName == packageName
- }.map {
- AppCrashesCount(it)
- }
- }
- .flowOn(defaultDispatcher)
- .stateIn(
- scope = viewModelScope,
- started = SharingStarted.Eagerly,
- initialValue = emptyList(),
- )
-
- fun deleteCrash(appCrash: AppCrash) = launchCatching {
- crashesRepository.delete(appCrash)
- }
-}
diff --git a/feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/viewmodel/list/CrashesViewModel.kt b/feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/viewmodel/list/CrashesViewModel.kt
deleted file mode 100644
index 6f3860c5..00000000
--- a/feature/crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/viewmodel/list/CrashesViewModel.kt
+++ /dev/null
@@ -1,150 +0,0 @@
-package com.f0x1d.logfox.feature.crashes.viewmodel.list
-
-import android.app.Application
-import android.content.Context
-import androidx.lifecycle.viewModelScope
-import com.f0x1d.logfox.arch.di.DefaultDispatcher
-import com.f0x1d.logfox.arch.viewmodel.BaseViewModel
-import com.f0x1d.logfox.database.entity.AppCrash
-import com.f0x1d.logfox.database.entity.AppCrashesCount
-import com.f0x1d.logfox.database.entity.DisabledApp
-import com.f0x1d.logfox.feature.apps.picker.viewmodel.AppsPickerResultHandler
-import com.f0x1d.logfox.feature.crashes.core.repository.CrashesRepository
-import com.f0x1d.logfox.feature.crashes.core.repository.DisabledAppsRepository
-import com.f0x1d.logfox.model.InstalledApp
-import com.f0x1d.logfox.preferences.shared.AppPreferences
-import com.f0x1d.logfox.preferences.shared.crashes.CrashesSort
-import com.f0x1d.logfox.strings.Strings
-import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.flow.update
-import kotlinx.coroutines.launch
-import javax.inject.Inject
-
-@HiltViewModel
-class CrashesViewModel @Inject constructor(
- private val crashesRepository: CrashesRepository,
- private val disabledAppsRepository: DisabledAppsRepository,
- private val appPreferences: AppPreferences,
- @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher,
- application: Application,
-): BaseViewModel(application), AppsPickerResultHandler {
-
- val currentSort get() = appPreferences.crashesSortType.get()
- val currentSortInReversedOrder get() = appPreferences.crashesSortReversedOrder.get()
-
- val crashes = combine(
- crashesRepository.getAllAsFlow(),
- appPreferences.crashesSortType.asFlow(),
- appPreferences.crashesSortReversedOrder.asFlow(),
- ) { crashes, sortType, sortInReversedOrder ->
- CrashesWithSort(
- crashes = crashes,
- sortType = sortType,
- sortInReversedOrder = sortInReversedOrder,
- )
- }
- .distinctUntilChanged()
- .map { crashesWithSort ->
- val groupedCrashes = crashesWithSort.crashes.groupBy { it.packageName }
-
- groupedCrashes.map {
- AppCrashesCount(
- lastCrash = it.value.first(),
- count = it.value.size
- )
- }.let(crashesWithSort.sortType.sorter).let { result ->
- if (crashesWithSort.sortInReversedOrder) {
- result.asReversed()
- } else {
- result
- }
- }
- }
- .flowOn(defaultDispatcher)
- .stateIn(
- scope = viewModelScope,
- started = SharingStarted.Eagerly,
- initialValue = emptyList(),
- )
-
- val query = MutableStateFlow("")
-
- val searchedCrashes = combine(
- crashesRepository.getAllAsFlow(),
- query,
- ) { crashes, query -> crashes to query }
- .map { (crashes, query) ->
- crashes.filter { crash ->
- crash.packageName.contains(query, ignoreCase = true)
- || crash.appName?.contains(query, ignoreCase = true) == true
- }.map { AppCrashesCount(it) }
- }
- .distinctUntilChanged()
- .flowOn(defaultDispatcher)
- .stateIn(
- scope = viewModelScope,
- started = SharingStarted.Eagerly,
- initialValue = emptyList(),
- )
-
- fun updateQuery(query: String) = this.query.update { query }
-
- fun updateSort(sortType: CrashesSort, sortInReversedOrder: Boolean) = appPreferences.updateCrashesSortSettings(
- sortType = sortType,
- sortInReversedOrder = sortInReversedOrder,
- )
-
- fun deleteCrashesByPackageName(appCrash: AppCrash) = launchCatching {
- crashesRepository.deleteAllByPackageName(appCrash)
- }
-
- fun deleteCrash(appCrash: AppCrash) = launchCatching {
- crashesRepository.delete(appCrash)
- }
-
- fun clearCrashes() = launchCatching {
- crashesRepository.clear()
- }
-
- override val supportsMultiplySelection: Boolean = true
-
- override val checkedAppPackageNames: Flow> =
- disabledAppsRepository.getAllAsFlow().map { apps ->
- apps.map(DisabledApp::packageName).toSet()
- }
-
- override fun providePickerTopAppBarTitle(context: Context): String =
- context.getString(Strings.blacklist)
-
- override fun onAppChecked(app: InstalledApp, checked: Boolean) {
- viewModelScope.launch {
- disabledAppsRepository.checkApp(app.packageName, checked)
- }
- }
-
- override fun onAppSelected(app: InstalledApp): Boolean {
- viewModelScope.launch {
- disabledAppsRepository.checkApp(app.packageName)
- }
- return false
- }
-
- private data class CrashesWithSort(
- val crashes: List,
- val sortType: CrashesSort,
- val sortInReversedOrder: Boolean,
- )
-
- companion object {
- private const val SEARCH_DEBOUNCE_MILLIS = 500L
- }
-}
diff --git a/feature/filters-core/.gitignore b/feature/filters/api/.gitignore
similarity index 100%
rename from feature/filters-core/.gitignore
rename to feature/filters/api/.gitignore
diff --git a/feature/logging-core/build.gradle.kts b/feature/filters/api/build.gradle.kts
similarity index 52%
rename from feature/logging-core/build.gradle.kts
rename to feature/filters/api/build.gradle.kts
index 6f91f895..82120138 100644
--- a/feature/logging-core/build.gradle.kts
+++ b/feature/filters/api/build.gradle.kts
@@ -2,7 +2,7 @@ plugins {
id("logfox.android.feature")
}
-android.namespace = "com.f0x1d.logfox.feature.logging.core"
+android.namespace = "com.f0x1d.logfox.feature.filters.api"
dependencies {
diff --git a/feature/filters/api/src/main/kotlin/com/f0x1d/logfox/feature/filters/api/data/FiltersRepository.kt b/feature/filters/api/src/main/kotlin/com/f0x1d/logfox/feature/filters/api/data/FiltersRepository.kt
new file mode 100644
index 00000000..04078610
--- /dev/null
+++ b/feature/filters/api/src/main/kotlin/com/f0x1d/logfox/feature/filters/api/data/FiltersRepository.kt
@@ -0,0 +1,38 @@
+package com.f0x1d.logfox.feature.filters.api.data
+
+import com.f0x1d.logfox.arch.repository.DatabaseProxyRepository
+import com.f0x1d.logfox.database.entity.UserFilter
+import com.f0x1d.logfox.model.logline.LogLevel
+import kotlinx.coroutines.flow.Flow
+
+interface FiltersRepository : DatabaseProxyRepository {
+
+ fun getAllEnabledAsFlow(): Flow>
+
+ suspend fun create(
+ including: Boolean,
+ enabledLogLevels: List,
+ uid: String?,
+ pid: String?,
+ tid: String?,
+ packageName: String?,
+ tag: String?,
+ content: String?,
+ )
+
+ suspend fun createAll(userFilters: List)
+
+ suspend fun switch(userFilter: UserFilter, checked: Boolean)
+
+ suspend fun update(
+ userFilter: UserFilter,
+ including: Boolean,
+ enabledLogLevels: List,
+ uid: String?,
+ pid: String?,
+ tid: String?,
+ packageName: String?,
+ tag: String?,
+ content: String?,
+ )
+}
diff --git a/feature/filters/build.gradle.kts b/feature/filters/build.gradle.kts
deleted file mode 100644
index 045b15ad..00000000
--- a/feature/filters/build.gradle.kts
+++ /dev/null
@@ -1,12 +0,0 @@
-plugins {
- id("logfox.android.feature")
-}
-
-android.namespace = "com.f0x1d.logfox.feature.filters"
-
-dependencies {
- implementation(projects.feature.appsPicker)
- implementation(projects.feature.filtersCore)
-
- implementation(libs.gson)
-}
diff --git a/feature/filters/edit/.gitignore b/feature/filters/edit/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/feature/filters/edit/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/feature/filters/edit/build.gradle.kts b/feature/filters/edit/build.gradle.kts
new file mode 100644
index 00000000..922ec355
--- /dev/null
+++ b/feature/filters/edit/build.gradle.kts
@@ -0,0 +1,12 @@
+plugins {
+ id("logfox.android.feature")
+}
+
+android.namespace = "com.f0x1d.logfox.feature.filters.edit"
+
+dependencies {
+ implementation(projects.feature.appsPicker.api)
+ implementation(projects.feature.filters.api)
+
+ implementation(libs.gson)
+}
diff --git a/feature/filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/di/EditFilterViewModelModule.kt b/feature/filters/edit/src/main/kotlin/com/f0x1d/logfox/feature/filters/edit/di/EditFilterViewModelModule.kt
similarity index 92%
rename from feature/filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/di/EditFilterViewModelModule.kt
rename to feature/filters/edit/src/main/kotlin/com/f0x1d/logfox/feature/filters/edit/di/EditFilterViewModelModule.kt
index 32188533..5ff3d8d9 100644
--- a/feature/filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/di/EditFilterViewModelModule.kt
+++ b/feature/filters/edit/src/main/kotlin/com/f0x1d/logfox/feature/filters/edit/di/EditFilterViewModelModule.kt
@@ -1,4 +1,4 @@
-package com.f0x1d.logfox.feature.filters.di
+package com.f0x1d.logfox.feature.filters.edit.di
import androidx.lifecycle.SavedStateHandle
import dagger.Module
diff --git a/feature/filters/edit/src/main/kotlin/com/f0x1d/logfox/feature/filters/edit/presentation/EditFilterAction.kt b/feature/filters/edit/src/main/kotlin/com/f0x1d/logfox/feature/filters/edit/presentation/EditFilterAction.kt
new file mode 100644
index 00000000..c4ef1a3f
--- /dev/null
+++ b/feature/filters/edit/src/main/kotlin/com/f0x1d/logfox/feature/filters/edit/presentation/EditFilterAction.kt
@@ -0,0 +1,5 @@
+package com.f0x1d.logfox.feature.filters.edit.presentation
+
+sealed interface EditFilterAction {
+ data class UpdatePackageNameText(val packageName: String) : EditFilterAction
+}
diff --git a/feature/filters/edit/src/main/kotlin/com/f0x1d/logfox/feature/filters/edit/presentation/EditFilterState.kt b/feature/filters/edit/src/main/kotlin/com/f0x1d/logfox/feature/filters/edit/presentation/EditFilterState.kt
new file mode 100644
index 00000000..ed3d3ce5
--- /dev/null
+++ b/feature/filters/edit/src/main/kotlin/com/f0x1d/logfox/feature/filters/edit/presentation/EditFilterState.kt
@@ -0,0 +1,9 @@
+package com.f0x1d.logfox.feature.filters.edit.presentation
+
+import com.f0x1d.logfox.database.entity.UserFilter
+
+data class EditFilterState(
+ val filter: UserFilter? = null,
+ val including: Boolean = true,
+ val enabledLogLevels: List = emptyList(),
+)
diff --git a/feature/filters/edit/src/main/kotlin/com/f0x1d/logfox/feature/filters/edit/presentation/EditFilterViewModel.kt b/feature/filters/edit/src/main/kotlin/com/f0x1d/logfox/feature/filters/edit/presentation/EditFilterViewModel.kt
new file mode 100644
index 00000000..c1319cfc
--- /dev/null
+++ b/feature/filters/edit/src/main/kotlin/com/f0x1d/logfox/feature/filters/edit/presentation/EditFilterViewModel.kt
@@ -0,0 +1,133 @@
+package com.f0x1d.logfox.feature.filters.edit.presentation
+
+import android.app.Application
+import android.net.Uri
+import androidx.lifecycle.viewModelScope
+import com.f0x1d.logfox.arch.di.IODispatcher
+import com.f0x1d.logfox.arch.viewmodel.BaseViewModel
+import com.f0x1d.logfox.feature.apps.picker.AppsPickerResultHandler
+import com.f0x1d.logfox.feature.apps.picker.InstalledApp
+import com.f0x1d.logfox.feature.filters.api.data.FiltersRepository
+import com.f0x1d.logfox.model.logline.LogLevel
+import com.google.gson.Gson
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.take
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class EditFilterViewModel @Inject constructor(
+ @com.f0x1d.logfox.feature.filters.edit.di.FilterId val filterId: Long?,
+ private val filtersRepository: FiltersRepository,
+ private val gson: Gson,
+ @IODispatcher private val ioDispatcher: CoroutineDispatcher,
+ application: Application,
+): BaseViewModel(
+ initialStateProvider = { EditFilterState() },
+ application = application,
+), AppsPickerResultHandler {
+ var uid: String? = null
+ var pid: String? = null
+ var tid: String? = null
+ var packageName: String? = null
+ var tag: String? = null
+ var content: String? = null
+
+ init {
+ load()
+ }
+
+ private fun load() {
+ viewModelScope.launch {
+ filtersRepository.getByIdAsFlow(filterId ?: -1L)
+ .distinctUntilChanged()
+ .take(1) // Not to handle changes
+ .collect { filter ->
+ if (filter == null) return@collect
+
+ val enabledLogLevels = List(7) { false }.toMutableList()
+ val allowedLevels = filter.allowedLevels.map { it.ordinal }
+ for (i in 0 until enabledLogLevels.size) {
+ enabledLogLevels[i] = allowedLevels.contains(i)
+ }
+
+ uid = filter.uid
+ pid = filter.pid
+ tid = filter.tid
+ packageName = filter.packageName
+ tag = filter.tag
+ content = filter.content
+
+ reduce {
+ copy(
+ filter = filter,
+ including = filter.including,
+ enabledLogLevels = enabledLogLevels,
+ )
+ }
+ }
+ }
+ }
+
+ fun save() = launchCatching {
+ val state = currentState
+
+ if (state.filter == null) {
+ filtersRepository.create(
+ including = state.including,
+ enabledLogLevels = state.enabledLogLevels.toEnabledLogLevels(),
+ uid = uid,
+ pid = pid,
+ tid = tid,
+ packageName = packageName,
+ tag = tag,
+ content = content,
+ )
+ } else {
+ filtersRepository.update(
+ userFilter = state.filter,
+ including = state.including,
+ enabledLogLevels = state.enabledLogLevels.toEnabledLogLevels(),
+ uid = uid,
+ pid = pid,
+ tid = tid,
+ packageName = packageName,
+ tag = tag,
+ content = content,
+ )
+ }
+ }
+
+ fun export(uri: Uri) = launchCatching(ioDispatcher) {
+ ctx.contentResolver.openOutputStream(uri)?.use { outputStream ->
+ val filters = listOfNotNull(currentState.filter)
+
+ outputStream.write(gson.toJson(filters).encodeToByteArray())
+ }
+ }
+
+ fun toggleIncluding() = reduce { copy(including = including.not()) }
+
+ fun filterLevel(which: Int, filtering: Boolean) = reduce {
+ copy(
+ enabledLogLevels = enabledLogLevels.toMutableList().apply {
+ this[which] = filtering
+ },
+ )
+ }
+
+ override fun onAppSelected(app: InstalledApp): Boolean {
+ packageName = app.packageName
+ sendAction(EditFilterAction.UpdatePackageNameText(app.packageName))
+ return true
+ }
+
+ private fun List.toEnabledLogLevels() = mapIndexed { index, value ->
+ if (value)
+ enumValues()[index]
+ else
+ null
+ }.filterNotNull()
+}
diff --git a/feature/filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/ui/fragment/EditFilterFragment.kt b/feature/filters/edit/src/main/kotlin/com/f0x1d/logfox/feature/filters/edit/presentation/ui/EditFilterFragment.kt
similarity index 60%
rename from feature/filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/ui/fragment/EditFilterFragment.kt
rename to feature/filters/edit/src/main/kotlin/com/f0x1d/logfox/feature/filters/edit/presentation/ui/EditFilterFragment.kt
index 3fa577c6..3434d8ad 100644
--- a/feature/filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/ui/fragment/EditFilterFragment.kt
+++ b/feature/filters/edit/src/main/kotlin/com/f0x1d/logfox/feature/filters/edit/presentation/ui/EditFilterFragment.kt
@@ -1,21 +1,19 @@
-package com.f0x1d.logfox.feature.filters.ui.fragment
+package com.f0x1d.logfox.feature.filters.edit.presentation.ui
import android.content.res.ColorStateList
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import android.widget.EditText
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.widget.doAfterTextChanged
import androidx.hilt.navigation.fragment.hiltNavGraphViewModels
import androidx.navigation.fragment.findNavController
-import com.f0x1d.logfox.arch.ui.fragment.BaseViewModelFragment
-import com.f0x1d.logfox.arch.viewmodel.Event
-import com.f0x1d.logfox.feature.filters.R
-import com.f0x1d.logfox.feature.filters.databinding.FragmentEditFilterBinding
-import com.f0x1d.logfox.feature.filters.viewmodel.EditFilterViewModel
-import com.f0x1d.logfox.feature.filters.viewmodel.UpdatePackageNameText
+import com.f0x1d.logfox.arch.presentation.ui.fragment.BaseFragment
+import com.f0x1d.logfox.feature.filters.edit.R
+import com.f0x1d.logfox.feature.filters.edit.databinding.FragmentEditFilterBinding
+import com.f0x1d.logfox.feature.filters.edit.presentation.EditFilterAction
+import com.f0x1d.logfox.feature.filters.edit.presentation.EditFilterViewModel
import com.f0x1d.logfox.model.logline.LogLevel
import com.f0x1d.logfox.navigation.Directions
import com.f0x1d.logfox.strings.Strings
@@ -26,14 +24,11 @@ import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import dev.chrisbanes.insetter.applyInsetter
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.take
-import kotlinx.coroutines.flow.update
@AndroidEntryPoint
-class EditFilterFragment: BaseViewModelFragment() {
+class EditFilterFragment : BaseFragment() {
- override val viewModel by hiltNavGraphViewModels(Directions.editFilterFragment)
+ private val viewModel by hiltNavGraphViewModels(Directions.editFilterFragment)
private val exportFilterLauncher = registerForActivityResult(
ActivityResultContracts.CreateDocument("application/json"),
@@ -68,9 +63,14 @@ class EditFilterFragment: BaseViewModelFragment
- updateIncludingButton(enabled)
- }
-
- viewModel.filter.collectWithLifecycle {
- viewModel.uid.toText(uidText)
- viewModel.pid.toText(pidText)
- viewModel.tid.toText(tidText)
- viewModel.packageName.toText(packageNameText)
- viewModel.tag.toText(tagText)
- viewModel.content.toText(contentText)
+ uidText.doAfterTextChanged { viewModel.uid = it?.toString().orEmpty() }
+ pidText.doAfterTextChanged { viewModel.pid = it?.toString().orEmpty() }
+ tidText.doAfterTextChanged { viewModel.tid = it?.toString().orEmpty() }
+ packageNameText.doAfterTextChanged { viewModel.packageName = it?.toString().orEmpty() }
+ tagText.doAfterTextChanged { viewModel.tag = it?.toString().orEmpty() }
+ contentText.doAfterTextChanged { viewModel.tag = it?.toString().orEmpty() }
- if (it == null) return@collectWithLifecycle
+ viewModel.state.collectWithLifecycle { state ->
+ updateIncludingButton(state.including)
- toolbar.menu.apply {
- findItem(R.id.export_item).isVisible = true
+ uidText.setText(viewModel.uid.orEmpty())
+ pidText.setText(viewModel.pid.orEmpty())
+ tidText.setText(viewModel.tid.orEmpty())
+ packageNameText.setText(viewModel.packageName.orEmpty())
+ tagText.setText(viewModel.tag.orEmpty())
+ contentText.setText(viewModel.content.orEmpty())
- setClickListenerOn(R.id.export_item) {
- exportFilterLauncher.launch("filter.json")
- }
- }
-
- saveFab.setOnClickListener { _ ->
- viewModel.update(it)
- findNavController().popBackStack()
- }
+ toolbar.menu.findItem(R.id.export_item).isVisible = state.filter != null
}
- }
-
- override fun onEvent(event: Event) {
- super.onEvent(event)
- when (event) {
- is UpdatePackageNameText -> {
- binding.packageNameText.setText(viewModel.packageName.value)
- }
- }
- }
-
- private fun MutableStateFlow.toText(editText: EditText) {
- take(1).collectWithLifecycle {
- editText.apply {
- setText(it)
- doAfterTextChanged { value -> update { value?.toString() } }
+ viewModel.actions.collectWithLifecycle { action ->
+ when (action) {
+ is EditFilterAction.UpdatePackageNameText -> {
+ binding.packageNameText.setText(action.packageName)
+ }
}
}
}
@@ -161,7 +142,7 @@ class EditFilterFragment: BaseViewModelFragment
viewModel.filterLevel(which, checked)
}
diff --git a/feature/filters/src/main/res/layout/fragment_edit_filter.xml b/feature/filters/edit/src/main/res/layout/fragment_edit_filter.xml
similarity index 100%
rename from feature/filters/src/main/res/layout/fragment_edit_filter.xml
rename to feature/filters/edit/src/main/res/layout/fragment_edit_filter.xml
diff --git a/feature/filters/src/main/res/menu/edit_filter_menu.xml b/feature/filters/edit/src/main/res/menu/edit_filter_menu.xml
similarity index 89%
rename from feature/filters/src/main/res/menu/edit_filter_menu.xml
rename to feature/filters/edit/src/main/res/menu/edit_filter_menu.xml
index b3557327..e6721418 100644
--- a/feature/filters/src/main/res/menu/edit_filter_menu.xml
+++ b/feature/filters/edit/src/main/res/menu/edit_filter_menu.xml
@@ -6,7 +6,6 @@
android:id="@+id/export_item"
android:title="@string/export"
android:icon="@drawable/ic_export"
- android:visible="false"
app:showAsAction="always" />
-
\ No newline at end of file
+
diff --git a/feature/filters/.gitignore b/feature/filters/impl/.gitignore
similarity index 100%
rename from feature/filters/.gitignore
rename to feature/filters/impl/.gitignore
diff --git a/feature/filters/impl/build.gradle.kts b/feature/filters/impl/build.gradle.kts
new file mode 100644
index 00000000..b49ee523
--- /dev/null
+++ b/feature/filters/impl/build.gradle.kts
@@ -0,0 +1,12 @@
+plugins {
+ id("logfox.android.feature")
+}
+
+android.namespace = "com.f0x1d.logfox.feature.filters.impl"
+
+dependencies {
+ implementation(projects.feature.appsPicker.api)
+ implementation(projects.feature.filters.api)
+
+ implementation(libs.gson)
+}
diff --git a/feature/filters-core/src/main/kotlin/com/f0x1d/logfox/feature/filters/core/repository/FiltersRepository.kt b/feature/filters/impl/src/main/kotlin/com/f0x1d/logfox/feature/filters/impl/data/FiltersRepositoryImpl.kt
similarity index 79%
rename from feature/filters-core/src/main/kotlin/com/f0x1d/logfox/feature/filters/core/repository/FiltersRepository.kt
rename to feature/filters/impl/src/main/kotlin/com/f0x1d/logfox/feature/filters/impl/data/FiltersRepositoryImpl.kt
index 18f75bb8..50f11a02 100644
--- a/feature/filters-core/src/main/kotlin/com/f0x1d/logfox/feature/filters/core/repository/FiltersRepository.kt
+++ b/feature/filters/impl/src/main/kotlin/com/f0x1d/logfox/feature/filters/impl/data/FiltersRepositoryImpl.kt
@@ -1,9 +1,9 @@
-package com.f0x1d.logfox.feature.filters.core.repository
+package com.f0x1d.logfox.feature.filters.impl.data
import com.f0x1d.logfox.arch.di.IODispatcher
-import com.f0x1d.logfox.arch.repository.DatabaseProxyRepository
import com.f0x1d.logfox.database.AppDatabase
import com.f0x1d.logfox.database.entity.UserFilter
+import com.f0x1d.logfox.feature.filters.api.data.FiltersRepository
import com.f0x1d.logfox.model.logline.LogLevel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
@@ -12,38 +12,6 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.withContext
import javax.inject.Inject
-interface FiltersRepository : DatabaseProxyRepository {
-
- fun getAllEnabledAsFlow(): Flow>
-
- suspend fun create(
- including: Boolean,
- enabledLogLevels: List,
- uid: String?,
- pid: String?,
- tid: String?,
- packageName: String?,
- tag: String?,
- content: String?,
- )
-
- suspend fun createAll(userFilters: List)
-
- suspend fun switch(userFilter: UserFilter, checked: Boolean)
-
- suspend fun update(
- userFilter: UserFilter,
- including: Boolean,
- enabledLogLevels: List,
- uid: String?,
- pid: String?,
- tid: String?,
- packageName: String?,
- tag: String?,
- content: String?,
- )
-}
-
internal class FiltersRepositoryImpl @Inject constructor(
private val database: AppDatabase,
@IODispatcher private val ioDispatcher: CoroutineDispatcher,
diff --git a/feature/filters-core/src/main/kotlin/com/f0x1d/logfox/feature/filters/core/di/RepositoriesModule.kt b/feature/filters/impl/src/main/kotlin/com/f0x1d/logfox/feature/filters/impl/di/RepositoriesModule.kt
similarity index 62%
rename from feature/filters-core/src/main/kotlin/com/f0x1d/logfox/feature/filters/core/di/RepositoriesModule.kt
rename to feature/filters/impl/src/main/kotlin/com/f0x1d/logfox/feature/filters/impl/di/RepositoriesModule.kt
index bb1dbff0..3a44cfd9 100644
--- a/feature/filters-core/src/main/kotlin/com/f0x1d/logfox/feature/filters/core/di/RepositoriesModule.kt
+++ b/feature/filters/impl/src/main/kotlin/com/f0x1d/logfox/feature/filters/impl/di/RepositoriesModule.kt
@@ -1,7 +1,7 @@
-package com.f0x1d.logfox.feature.filters.core.di
+package com.f0x1d.logfox.feature.filters.impl.di
-import com.f0x1d.logfox.feature.filters.core.repository.FiltersRepository
-import com.f0x1d.logfox.feature.filters.core.repository.FiltersRepositoryImpl
+import com.f0x1d.logfox.feature.filters.api.data.FiltersRepository
+import com.f0x1d.logfox.feature.filters.impl.data.FiltersRepositoryImpl
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
diff --git a/feature/filters/list/.gitignore b/feature/filters/list/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/feature/filters/list/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/feature/filters/list/build.gradle.kts b/feature/filters/list/build.gradle.kts
new file mode 100644
index 00000000..aef3028f
--- /dev/null
+++ b/feature/filters/list/build.gradle.kts
@@ -0,0 +1,11 @@
+plugins {
+ id("logfox.android.feature")
+}
+
+android.namespace = "com.f0x1d.logfox.feature.filters.list"
+
+dependencies {
+ implementation(projects.feature.filters.api)
+
+ implementation(libs.gson)
+}
diff --git a/feature/filters/list/src/main/kotlin/com/f0x1d/logfox/feature/filters/list/presentation/FiltersAction.kt b/feature/filters/list/src/main/kotlin/com/f0x1d/logfox/feature/filters/list/presentation/FiltersAction.kt
new file mode 100644
index 00000000..bbb92a0a
--- /dev/null
+++ b/feature/filters/list/src/main/kotlin/com/f0x1d/logfox/feature/filters/list/presentation/FiltersAction.kt
@@ -0,0 +1,3 @@
+package com.f0x1d.logfox.feature.filters.list.presentation
+
+sealed interface FiltersAction
diff --git a/feature/filters/list/src/main/kotlin/com/f0x1d/logfox/feature/filters/list/presentation/FiltersState.kt b/feature/filters/list/src/main/kotlin/com/f0x1d/logfox/feature/filters/list/presentation/FiltersState.kt
new file mode 100644
index 00000000..b00ef2f0
--- /dev/null
+++ b/feature/filters/list/src/main/kotlin/com/f0x1d/logfox/feature/filters/list/presentation/FiltersState.kt
@@ -0,0 +1,7 @@
+package com.f0x1d.logfox.feature.filters.list.presentation
+
+import com.f0x1d.logfox.database.entity.UserFilter
+
+data class FiltersState(
+ val filters: List = emptyList(),
+)
diff --git a/feature/filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/viewmodel/FiltersViewModel.kt b/feature/filters/list/src/main/kotlin/com/f0x1d/logfox/feature/filters/list/presentation/FiltersViewModel.kt
similarity index 71%
rename from feature/filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/viewmodel/FiltersViewModel.kt
rename to feature/filters/list/src/main/kotlin/com/f0x1d/logfox/feature/filters/list/presentation/FiltersViewModel.kt
index 91c1212b..a5a3118b 100644
--- a/feature/filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/viewmodel/FiltersViewModel.kt
+++ b/feature/filters/list/src/main/kotlin/com/f0x1d/logfox/feature/filters/list/presentation/FiltersViewModel.kt
@@ -1,4 +1,4 @@
-package com.f0x1d.logfox.feature.filters.viewmodel
+package com.f0x1d.logfox.feature.filters.list.presentation
import android.app.Application
import android.net.Uri
@@ -6,14 +6,13 @@ import androidx.lifecycle.viewModelScope
import com.f0x1d.logfox.arch.di.IODispatcher
import com.f0x1d.logfox.arch.viewmodel.BaseViewModel
import com.f0x1d.logfox.database.entity.UserFilter
-import com.f0x1d.logfox.feature.filters.core.repository.FiltersRepository
+import com.f0x1d.logfox.feature.filters.api.data.FiltersRepository
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
@@ -22,15 +21,23 @@ class FiltersViewModel @Inject constructor(
private val gson: Gson,
@IODispatcher private val ioDispatcher: CoroutineDispatcher,
application: Application,
-): BaseViewModel(application) {
+) : BaseViewModel(
+ initialStateProvider = { FiltersState() },
+ application = application,
+) {
+ init {
+ load()
+ }
- val filters = filtersRepository.getAllAsFlow()
- .distinctUntilChanged()
- .stateIn(
- scope = viewModelScope,
- started = SharingStarted.Eagerly,
- initialValue = emptyList(),
- )
+ private fun load() {
+ viewModelScope.launch {
+ filtersRepository.getAllAsFlow()
+ .distinctUntilChanged()
+ .collect { filters ->
+ reduce { copy(filters = filters) }
+ }
+ }
+ }
fun import(uri: Uri) = launchCatching(ioDispatcher) {
ctx.contentResolver.openInputStream(uri)?.use {
@@ -44,7 +51,7 @@ class FiltersViewModel @Inject constructor(
}
fun exportAll(uri: Uri) = launchCatching(ioDispatcher) {
- val filters = filters.value
+ val filters = currentState.filters
ctx.contentResolver.openOutputStream(uri)?.use {
it.write(gson.toJson(filters).encodeToByteArray())
diff --git a/feature/filters/list/src/main/kotlin/com/f0x1d/logfox/feature/filters/list/presentation/adapter/FiltersAdapter.kt b/feature/filters/list/src/main/kotlin/com/f0x1d/logfox/feature/filters/list/presentation/adapter/FiltersAdapter.kt
new file mode 100644
index 00000000..1e438b24
--- /dev/null
+++ b/feature/filters/list/src/main/kotlin/com/f0x1d/logfox/feature/filters/list/presentation/adapter/FiltersAdapter.kt
@@ -0,0 +1,24 @@
+package com.f0x1d.logfox.feature.filters.list.presentation.adapter
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import com.f0x1d.logfox.arch.presentation.adapter.BaseListAdapter
+import com.f0x1d.logfox.database.entity.UserFilter
+import com.f0x1d.logfox.feature.filters.list.databinding.ItemFilterBinding
+import com.f0x1d.logfox.feature.filters.list.presentation.ui.viewholder.FilterViewHolder
+import com.f0x1d.logfox.model.diffCallback
+
+class FiltersAdapter(
+ private val click: (UserFilter) -> Unit,
+ private val delete: (UserFilter) -> Unit,
+ private val checked: (UserFilter, Boolean) -> Unit
+) : BaseListAdapter(diffCallback()) {
+
+ override fun createHolder(layoutInflater: LayoutInflater, parent: ViewGroup) =
+ FilterViewHolder(
+ binding = ItemFilterBinding.inflate(layoutInflater, parent, false),
+ click = click,
+ delete = delete,
+ checked = checked,
+ )
+}
diff --git a/feature/filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/ui/fragment/FiltersFragment.kt b/feature/filters/list/src/main/kotlin/com/f0x1d/logfox/feature/filters/list/presentation/ui/fragment/FiltersFragment.kt
similarity index 81%
rename from feature/filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/ui/fragment/FiltersFragment.kt
rename to feature/filters/list/src/main/kotlin/com/f0x1d/logfox/feature/filters/list/presentation/ui/fragment/FiltersFragment.kt
index 31e1de03..93b6560c 100644
--- a/feature/filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/ui/fragment/FiltersFragment.kt
+++ b/feature/filters/list/src/main/kotlin/com/f0x1d/logfox/feature/filters/list/presentation/ui/fragment/FiltersFragment.kt
@@ -1,4 +1,4 @@
-package com.f0x1d.logfox.feature.filters.ui.fragment
+package com.f0x1d.logfox.feature.filters.list.presentation.ui.fragment
import android.os.Bundle
import android.view.LayoutInflater
@@ -11,11 +11,11 @@ import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.f0x1d.logfox.arch.canPickJSON
-import com.f0x1d.logfox.arch.ui.fragment.BaseViewModelFragment
-import com.f0x1d.logfox.feature.filters.R
-import com.f0x1d.logfox.feature.filters.adapter.FiltersAdapter
-import com.f0x1d.logfox.feature.filters.databinding.FragmentFiltersBinding
-import com.f0x1d.logfox.feature.filters.viewmodel.FiltersViewModel
+import com.f0x1d.logfox.arch.presentation.ui.fragment.BaseFragment
+import com.f0x1d.logfox.feature.filters.list.R
+import com.f0x1d.logfox.feature.filters.list.databinding.FragmentFiltersBinding
+import com.f0x1d.logfox.feature.filters.list.presentation.FiltersViewModel
+import com.f0x1d.logfox.feature.filters.list.presentation.adapter.FiltersAdapter
import com.f0x1d.logfox.navigation.Directions
import com.f0x1d.logfox.ui.dialog.showAreYouSureClearDialog
import com.f0x1d.logfox.ui.dialog.showAreYouSureDeleteDialog
@@ -25,9 +25,9 @@ import dagger.hilt.android.AndroidEntryPoint
import dev.chrisbanes.insetter.applyInsetter
@AndroidEntryPoint
-class FiltersFragment: BaseViewModelFragment() {
+class FiltersFragment : BaseFragment() {
- override val viewModel by viewModels()
+ private val viewModel by viewModels()
private val adapter = FiltersAdapter(
click = {
@@ -96,10 +96,10 @@ class FiltersFragment: BaseViewModelFragment
+ placeholderLayout.root.isVisible = state.filters.isEmpty()
- adapter.submitList(it)
+ adapter.submitList(state.filters)
}
}
}
diff --git a/feature/filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/ui/viewholder/FilterViewHolder.kt b/feature/filters/list/src/main/kotlin/com/f0x1d/logfox/feature/filters/list/presentation/ui/viewholder/FilterViewHolder.kt
similarity index 90%
rename from feature/filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/ui/viewholder/FilterViewHolder.kt
rename to feature/filters/list/src/main/kotlin/com/f0x1d/logfox/feature/filters/list/presentation/ui/viewholder/FilterViewHolder.kt
index 8e398cba..d5ffd249 100644
--- a/feature/filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/ui/viewholder/FilterViewHolder.kt
+++ b/feature/filters/list/src/main/kotlin/com/f0x1d/logfox/feature/filters/list/presentation/ui/viewholder/FilterViewHolder.kt
@@ -1,11 +1,11 @@
-package com.f0x1d.logfox.feature.filters.ui.viewholder
+package com.f0x1d.logfox.feature.filters.list.presentation.ui.viewholder
import android.text.Html
import android.view.View
import android.widget.TextView
-import com.f0x1d.logfox.arch.ui.viewholder.BaseViewHolder
+import com.f0x1d.logfox.arch.presentation.ui.viewholder.BaseViewHolder
import com.f0x1d.logfox.database.entity.UserFilter
-import com.f0x1d.logfox.feature.filters.databinding.ItemFilterBinding
+import com.f0x1d.logfox.feature.filters.list.databinding.ItemFilterBinding
import com.f0x1d.logfox.strings.Strings
import com.f0x1d.logfox.ui.view.OnlyUserCheckedChangeListener
diff --git a/feature/filters/src/main/res/layout/fragment_filters.xml b/feature/filters/list/src/main/res/layout/fragment_filters.xml
similarity index 100%
rename from feature/filters/src/main/res/layout/fragment_filters.xml
rename to feature/filters/list/src/main/res/layout/fragment_filters.xml
diff --git a/feature/filters/src/main/res/layout/item_filter.xml b/feature/filters/list/src/main/res/layout/item_filter.xml
similarity index 100%
rename from feature/filters/src/main/res/layout/item_filter.xml
rename to feature/filters/list/src/main/res/layout/item_filter.xml
diff --git a/feature/filters/src/main/res/layout/placeholder_filters.xml b/feature/filters/list/src/main/res/layout/placeholder_filters.xml
similarity index 100%
rename from feature/filters/src/main/res/layout/placeholder_filters.xml
rename to feature/filters/list/src/main/res/layout/placeholder_filters.xml
diff --git a/feature/filters/src/main/res/menu/filters_menu.xml b/feature/filters/list/src/main/res/menu/filters_menu.xml
similarity index 100%
rename from feature/filters/src/main/res/menu/filters_menu.xml
rename to feature/filters/list/src/main/res/menu/filters_menu.xml
diff --git a/feature/filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/adapter/FiltersAdapter.kt b/feature/filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/adapter/FiltersAdapter.kt
deleted file mode 100644
index e473e5aa..00000000
--- a/feature/filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/adapter/FiltersAdapter.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-package com.f0x1d.logfox.feature.filters.adapter
-
-import android.view.LayoutInflater
-import android.view.ViewGroup
-import androidx.recyclerview.widget.DiffUtil
-import com.f0x1d.logfox.arch.adapter.BaseListAdapter
-import com.f0x1d.logfox.database.entity.UserFilter
-import com.f0x1d.logfox.feature.filters.databinding.ItemFilterBinding
-import com.f0x1d.logfox.feature.filters.ui.viewholder.FilterViewHolder
-import com.f0x1d.logfox.model.diffCallback
-
-class FiltersAdapter(
- private val click: (UserFilter) -> Unit,
- private val delete: (UserFilter) -> Unit,
- private val checked: (UserFilter, Boolean) -> Unit
-): BaseListAdapter(diffCallback()) {
-
- companion object {
- private val FILTER_DIFF = object : DiffUtil.ItemCallback() {
- override fun areItemsTheSame(oldItem: UserFilter, newItem: UserFilter) = oldItem.id == newItem.id
-
- override fun areContentsTheSame(oldItem: UserFilter, newItem: UserFilter) = oldItem == newItem
- }
- }
-
- override fun createHolder(layoutInflater: LayoutInflater, parent: ViewGroup) = FilterViewHolder(
- binding = ItemFilterBinding.inflate(layoutInflater, parent, false),
- click = click,
- delete = delete,
- checked = checked,
- )
-}
diff --git a/feature/filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/viewmodel/EditFilterViewModel.kt b/feature/filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/viewmodel/EditFilterViewModel.kt
deleted file mode 100644
index 726de3cd..00000000
--- a/feature/filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/viewmodel/EditFilterViewModel.kt
+++ /dev/null
@@ -1,117 +0,0 @@
-package com.f0x1d.logfox.feature.filters.viewmodel
-
-import android.app.Application
-import android.net.Uri
-import androidx.lifecycle.viewModelScope
-import com.f0x1d.logfox.arch.di.IODispatcher
-import com.f0x1d.logfox.arch.viewmodel.BaseViewModel
-import com.f0x1d.logfox.arch.viewmodel.Event
-import com.f0x1d.logfox.database.entity.UserFilter
-import com.f0x1d.logfox.feature.apps.picker.viewmodel.AppsPickerResultHandler
-import com.f0x1d.logfox.feature.filters.core.repository.FiltersRepository
-import com.f0x1d.logfox.feature.filters.di.FilterId
-import com.f0x1d.logfox.model.InstalledApp
-import com.f0x1d.logfox.model.logline.LogLevel
-import com.google.gson.Gson
-import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.flow.take
-import kotlinx.coroutines.flow.update
-import javax.inject.Inject
-
-@HiltViewModel
-class EditFilterViewModel @Inject constructor(
- @FilterId val filterId: Long?,
- private val filtersRepository: FiltersRepository,
- private val gson: Gson,
- @IODispatcher private val ioDispatcher: CoroutineDispatcher,
- application: Application,
-): BaseViewModel(application), AppsPickerResultHandler {
-
- val filter = filtersRepository.getByIdAsFlow(filterId ?: -1L)
- .distinctUntilChanged()
- .take(1) // Not to handle changes
- .onEach { filter ->
- if (filter == null) return@onEach
-
- including.update { filter.including }
-
- val allowedLevels = filter.allowedLevels.map { it.ordinal }
- for (i in 0 until enabledLogLevels.size) {
- enabledLogLevels[i] = allowedLevels.contains(i)
- }
-
- uid.update { filter.uid }
- pid.update { filter.pid }
- tid.update { filter.tid }
- packageName.update { filter.packageName }
- tag.update { filter.tag }
- content.update { filter.content }
- }
- .stateIn(
- scope = viewModelScope,
- started = SharingStarted.Eagerly,
- initialValue = null,
- )
-
- val including = MutableStateFlow(true)
- val enabledLogLevels = mutableListOf(true, true, true, true, true, true, true)
- val uid = MutableStateFlow(null)
- val pid = MutableStateFlow(null)
- val tid = MutableStateFlow(null)
- val packageName = MutableStateFlow(null)
- val tag = MutableStateFlow(null)
- val content = MutableStateFlow(null)
-
- fun create() = launchCatching {
- filtersRepository.create(
- including.value,
- enabledLogLevels.toEnabledLogLevels(),
- uid.value, pid.value, tid.value, packageName.value, tag.value, content.value
- )
- }
-
- fun update(userFilter: UserFilter) = launchCatching {
- filtersRepository.update(
- userFilter,
- including.value,
- enabledLogLevels.toEnabledLogLevels(),
- uid.value, pid.value, tid.value, packageName.value, tag.value, content.value
- )
- }
-
- fun export(uri: Uri) = launchCatching(ioDispatcher) {
- ctx.contentResolver.openOutputStream(uri)?.use { outputStream ->
- val filters = filter.value?.let { listOf(it) } ?: emptyList()
-
- outputStream.write(gson.toJson(filters).encodeToByteArray())
- }
- }
-
- fun filterLevel(which: Int, filtering: Boolean) {
- enabledLogLevels[which] = filtering
- }
-
- override fun onAppSelected(app: InstalledApp): Boolean {
- packageName.update {
- app.packageName
- }.also {
- sendEvent(UpdatePackageNameText)
- }
- return true
- }
-
- private fun List.toEnabledLogLevels() = mapIndexed { index, value ->
- if (value)
- enumValues()[index]
- else
- null
- }.filterNotNull()
-}
-
-data object UpdatePackageNameText : Event
diff --git a/feature/logging-core/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/di/StoresModule.kt b/feature/logging-core/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/di/StoresModule.kt
deleted file mode 100644
index a94fac1e..00000000
--- a/feature/logging-core/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/di/StoresModule.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.f0x1d.logfox.feature.logging.core.di
-
-import com.f0x1d.logfox.feature.logging.core.store.LoggingStore
-import com.f0x1d.logfox.feature.logging.core.store.LoggingStoreImpl
-import dagger.Binds
-import dagger.Module
-import dagger.hilt.InstallIn
-import dagger.hilt.components.SingletonComponent
-
-@Module
-@InstallIn(SingletonComponent::class)
-internal interface StoresModule {
-
- @Binds
- fun bindLoggingStore(
- loggingStoreImpl: LoggingStoreImpl,
- ): LoggingStore
-}
diff --git a/feature/logging-core/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/repository/LoggingRepository.kt b/feature/logging-core/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/repository/LoggingRepository.kt
deleted file mode 100644
index bafe767d..00000000
--- a/feature/logging-core/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/repository/LoggingRepository.kt
+++ /dev/null
@@ -1,102 +0,0 @@
-package com.f0x1d.logfox.feature.logging.core.repository
-
-import android.content.Context
-import com.f0x1d.logfox.arch.di.IODispatcher
-import com.f0x1d.logfox.arch.repository.BaseRepository
-import com.f0x1d.logfox.model.exception.TerminalNotSupportedException
-import com.f0x1d.logfox.model.logline.LogLine
-import com.f0x1d.logfox.preferences.shared.AppPreferences
-import com.f0x1d.logfox.terminals.base.Terminal
-import dagger.hilt.android.qualifiers.ApplicationContext
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.FlowCollector
-import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.flowOn
-import javax.inject.Inject
-
-interface LoggingRepository {
-
- companion object {
- val COMMAND = arrayOf("logcat" , "-v", "uid", "-v", "epoch")
-
- val DUMP_FLAG = arrayOf("-d")
- val SHOW_LOGS_FROM_NOW_FLAGS = arrayOf("-T", "1")
- }
-
- fun startLogging(
- terminal: Terminal,
- startingId: Long = 0,
- ): Flow
-
- fun dumpLogs(terminal: Terminal): Flow
-}
-
-internal class LoggingRepositoryImpl @Inject constructor(
- @ApplicationContext private val context: Context,
- private val appPreferences: AppPreferences,
- @IODispatcher private val ioDispatcher: CoroutineDispatcher,
-): BaseRepository(), LoggingRepository {
-
- override fun startLogging(
- terminal: Terminal,
- startingId: Long,
- ): Flow = flow {
- val command = LoggingRepository.COMMAND + when (appPreferences.showLogsFromAppLaunch) {
- true -> LoggingRepository.SHOW_LOGS_FROM_NOW_FLAGS
-
- else -> emptyArray()
- }
-
- emitLines(
- terminal = terminal,
- command = command,
- startingId = startingId,
- )
- }.flowOn(ioDispatcher)
-
- override fun dumpLogs(terminal: Terminal): Flow = flow {
- val command = LoggingRepository.COMMAND + LoggingRepository.DUMP_FLAG
-
- emitLines(
- terminal = terminal,
- command = command,
- startingId = 0,
- )
- }.flowOn(ioDispatcher)
-
- private suspend fun FlowCollector.emitLines(
- terminal: Terminal,
- command: Array,
- startingId: Long = 0,
- ) {
- if (terminal.isSupported().not()) {
- throw TerminalNotSupportedException()
- }
-
- val process = terminal.execute(*command) ?: throw TerminalNotSupportedException()
-
- var idsCounter = startingId
-
- try {
- process.output.bufferedReader().useLines {
- var droppedFirst = !appPreferences.showLogsFromAppLaunch
- // avoiding getting the same line after logging restart because of
- // WARNING: -T 0 invalid, setting to 1
- for (line in it) {
- val logLine = LogLine(idsCounter++, line, context) ?: continue
- if (!droppedFirst) {
- droppedFirst = true
- continue
- }
-
- emit(logLine)
- }
- }
- } finally {
- runCatching {
- process.destroy()
- }
- }
- }
-}
diff --git a/feature/logging-core/.gitignore b/feature/logging/api/.gitignore
similarity index 100%
rename from feature/logging-core/.gitignore
rename to feature/logging/api/.gitignore
diff --git a/feature/filters-core/build.gradle.kts b/feature/logging/api/build.gradle.kts
similarity index 52%
rename from feature/filters-core/build.gradle.kts
rename to feature/logging/api/build.gradle.kts
index cddf00c9..3bc15241 100644
--- a/feature/filters-core/build.gradle.kts
+++ b/feature/logging/api/build.gradle.kts
@@ -2,7 +2,7 @@ plugins {
id("logfox.android.feature")
}
-android.namespace = "com.f0x1d.logfox.feature.filters.core"
+android.namespace = "com.f0x1d.logfox.feature.logging.api"
dependencies {
diff --git a/feature/logging/api/src/main/kotlin/com/f0x1d/logfox/feature/logging/api/data/LoggingRepository.kt b/feature/logging/api/src/main/kotlin/com/f0x1d/logfox/feature/logging/api/data/LoggingRepository.kt
new file mode 100644
index 00000000..aa976498
--- /dev/null
+++ b/feature/logging/api/src/main/kotlin/com/f0x1d/logfox/feature/logging/api/data/LoggingRepository.kt
@@ -0,0 +1,22 @@
+package com.f0x1d.logfox.feature.logging.api.data
+
+import com.f0x1d.logfox.model.logline.LogLine
+import com.f0x1d.logfox.terminals.base.Terminal
+import kotlinx.coroutines.flow.Flow
+
+interface LoggingRepository {
+
+ companion object {
+ val COMMAND = arrayOf("logcat" , "-v", "uid", "-v", "epoch")
+
+ val DUMP_FLAG = arrayOf("-d")
+ val SHOW_LOGS_FROM_NOW_FLAGS = arrayOf("-T", "1")
+ }
+
+ fun startLogging(
+ terminal: Terminal,
+ startingId: Long = 0,
+ ): Flow
+
+ fun dumpLogs(terminal: Terminal): Flow
+}
diff --git a/feature/logging/api/src/main/kotlin/com/f0x1d/logfox/feature/logging/api/data/LogsDataSource.kt b/feature/logging/api/src/main/kotlin/com/f0x1d/logfox/feature/logging/api/data/LogsDataSource.kt
new file mode 100644
index 00000000..63c04abd
--- /dev/null
+++ b/feature/logging/api/src/main/kotlin/com/f0x1d/logfox/feature/logging/api/data/LogsDataSource.kt
@@ -0,0 +1,10 @@
+package com.f0x1d.logfox.feature.logging.api.data
+
+import com.f0x1d.logfox.model.logline.LogLine
+import kotlinx.coroutines.flow.Flow
+
+interface LogsDataSource {
+ val logs: Flow>
+
+ suspend fun updateLogs(logs: List)
+}
diff --git a/feature/logging/api/src/main/kotlin/com/f0x1d/logfox/feature/logging/api/data/QueryDataSource.kt b/feature/logging/api/src/main/kotlin/com/f0x1d/logfox/feature/logging/api/data/QueryDataSource.kt
new file mode 100644
index 00000000..da6df653
--- /dev/null
+++ b/feature/logging/api/src/main/kotlin/com/f0x1d/logfox/feature/logging/api/data/QueryDataSource.kt
@@ -0,0 +1,9 @@
+package com.f0x1d.logfox.feature.logging.api.data
+
+import kotlinx.coroutines.flow.Flow
+
+interface QueryDataSource {
+ val query: Flow
+
+ suspend fun updateQuery(query: String?)
+}
diff --git a/feature/logging/api/src/main/kotlin/com/f0x1d/logfox/feature/logging/api/data/SelectedLogLinesDataSource.kt b/feature/logging/api/src/main/kotlin/com/f0x1d/logfox/feature/logging/api/data/SelectedLogLinesDataSource.kt
new file mode 100644
index 00000000..3645bd35
--- /dev/null
+++ b/feature/logging/api/src/main/kotlin/com/f0x1d/logfox/feature/logging/api/data/SelectedLogLinesDataSource.kt
@@ -0,0 +1,10 @@
+package com.f0x1d.logfox.feature.logging.api.data
+
+import com.f0x1d.logfox.model.logline.LogLine
+import kotlinx.coroutines.flow.Flow
+
+interface SelectedLogLinesDataSource {
+ val selectedLines: Flow>
+
+ suspend fun updateSelectedLines(selectedLines: List)
+}
diff --git a/feature/logging-core/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/model/LogLinesExt.kt b/feature/logging/api/src/main/kotlin/com/f0x1d/logfox/feature/logging/api/model/LogLinesExt.kt
similarity index 97%
rename from feature/logging-core/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/model/LogLinesExt.kt
rename to feature/logging/api/src/main/kotlin/com/f0x1d/logfox/feature/logging/api/model/LogLinesExt.kt
index 9f0204f0..92068f41 100644
--- a/feature/logging-core/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/model/LogLinesExt.kt
+++ b/feature/logging/api/src/main/kotlin/com/f0x1d/logfox/feature/logging/api/model/LogLinesExt.kt
@@ -1,4 +1,4 @@
-package com.f0x1d.logfox.feature.logging.core.model
+package com.f0x1d.logfox.feature.logging.api.model
import com.f0x1d.logfox.database.entity.UserFilter
import com.f0x1d.logfox.model.logline.LogLine
diff --git a/feature/logging/api/src/main/kotlin/com/f0x1d/logfox/feature/logging/api/presentation/LoggingServiceDelegate.kt b/feature/logging/api/src/main/kotlin/com/f0x1d/logfox/feature/logging/api/presentation/LoggingServiceDelegate.kt
new file mode 100644
index 00000000..4996713a
--- /dev/null
+++ b/feature/logging/api/src/main/kotlin/com/f0x1d/logfox/feature/logging/api/presentation/LoggingServiceDelegate.kt
@@ -0,0 +1,7 @@
+package com.f0x1d.logfox.feature.logging.api.presentation
+
+interface LoggingServiceDelegate {
+ fun clearLogs()
+ fun restartLogging()
+ fun killService()
+}
diff --git a/feature/logging/build.gradle.kts b/feature/logging/build.gradle.kts
deleted file mode 100644
index 26490df9..00000000
--- a/feature/logging/build.gradle.kts
+++ /dev/null
@@ -1,12 +0,0 @@
-plugins {
- id("logfox.android.feature")
-}
-
-android.namespace = "com.f0x1d.logfox.feature.logging"
-
-dependencies {
- implementation(projects.feature.crashesCore)
- implementation(projects.feature.filtersCore)
- implementation(projects.feature.loggingCore)
- implementation(projects.feature.recordingsCore)
-}
diff --git a/feature/recordings-core/.gitignore b/feature/logging/extended-copy/.gitignore
similarity index 100%
rename from feature/recordings-core/.gitignore
rename to feature/logging/extended-copy/.gitignore
diff --git a/feature/logging/extended-copy/build.gradle.kts b/feature/logging/extended-copy/build.gradle.kts
new file mode 100644
index 00000000..a76d5f33
--- /dev/null
+++ b/feature/logging/extended-copy/build.gradle.kts
@@ -0,0 +1,9 @@
+plugins {
+ id("logfox.android.feature")
+}
+
+android.namespace = "com.f0x1d.logfox.feature.logging.extended.copy"
+
+dependencies {
+ implementation(projects.feature.logging.api)
+}
diff --git a/feature/logging/extended-copy/src/main/kotlin/com/f0x1d/logfox/feature/logging/extended/copy/presentation/LogsExtendedCopyAction.kt b/feature/logging/extended-copy/src/main/kotlin/com/f0x1d/logfox/feature/logging/extended/copy/presentation/LogsExtendedCopyAction.kt
new file mode 100644
index 00000000..cf25e416
--- /dev/null
+++ b/feature/logging/extended-copy/src/main/kotlin/com/f0x1d/logfox/feature/logging/extended/copy/presentation/LogsExtendedCopyAction.kt
@@ -0,0 +1,3 @@
+package com.f0x1d.logfox.feature.logging.extended.copy.presentation
+
+sealed interface LogsExtendedCopyAction
diff --git a/feature/logging/extended-copy/src/main/kotlin/com/f0x1d/logfox/feature/logging/extended/copy/presentation/LogsExtendedCopyState.kt b/feature/logging/extended-copy/src/main/kotlin/com/f0x1d/logfox/feature/logging/extended/copy/presentation/LogsExtendedCopyState.kt
new file mode 100644
index 00000000..37ed0038
--- /dev/null
+++ b/feature/logging/extended-copy/src/main/kotlin/com/f0x1d/logfox/feature/logging/extended/copy/presentation/LogsExtendedCopyState.kt
@@ -0,0 +1,5 @@
+package com.f0x1d.logfox.feature.logging.extended.copy.presentation
+
+data class LogsExtendedCopyState(
+ val text: String? = null,
+)
diff --git a/feature/logging/extended-copy/src/main/kotlin/com/f0x1d/logfox/feature/logging/extended/copy/presentation/LogsExtendedCopyViewModel.kt b/feature/logging/extended-copy/src/main/kotlin/com/f0x1d/logfox/feature/logging/extended/copy/presentation/LogsExtendedCopyViewModel.kt
new file mode 100644
index 00000000..1c7c00da
--- /dev/null
+++ b/feature/logging/extended-copy/src/main/kotlin/com/f0x1d/logfox/feature/logging/extended/copy/presentation/LogsExtendedCopyViewModel.kt
@@ -0,0 +1,50 @@
+package com.f0x1d.logfox.feature.logging.extended.copy.presentation
+
+import android.app.Application
+import androidx.lifecycle.viewModelScope
+import com.f0x1d.logfox.arch.di.DefaultDispatcher
+import com.f0x1d.logfox.arch.viewmodel.BaseViewModel
+import com.f0x1d.logfox.datetime.DateTimeFormatter
+import com.f0x1d.logfox.feature.logging.api.data.SelectedLogLinesDataSource
+import com.f0x1d.logfox.preferences.shared.AppPreferences
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class LogsExtendedCopyViewModel @Inject constructor(
+ private val selectedLogLinesDataSource: SelectedLogLinesDataSource,
+ private val appPreferences: AppPreferences,
+ @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher,
+ dateTimeFormatter: DateTimeFormatter,
+ application: Application,
+) : BaseViewModel(
+ initialStateProvider = { LogsExtendedCopyState() },
+ application = application,
+), DateTimeFormatter by dateTimeFormatter {
+ init {
+ load()
+ }
+
+ private fun load() {
+ viewModelScope.launch {
+ selectedLogLinesDataSource.selectedLines
+ .map { lines ->
+ lines.joinToString("\n") { line ->
+ appPreferences.originalOf(
+ logLine = line,
+ formatDate = ::formatDate,
+ formatTime = ::formatTime,
+ )
+ }
+ }
+ .flowOn(defaultDispatcher)
+ .collect { text ->
+ reduce { copy(text = text) }
+ }
+ }
+ }
+}
diff --git a/feature/logging/src/main/kotlin/com/f0x1d/feature/logging/ui/fragment/LogsExtendedCopyFragment.kt b/feature/logging/extended-copy/src/main/kotlin/com/f0x1d/logfox/feature/logging/extended/copy/presentation/ui/LogsExtendedCopyFragment.kt
similarity index 50%
rename from feature/logging/src/main/kotlin/com/f0x1d/feature/logging/ui/fragment/LogsExtendedCopyFragment.kt
rename to feature/logging/extended-copy/src/main/kotlin/com/f0x1d/logfox/feature/logging/extended/copy/presentation/ui/LogsExtendedCopyFragment.kt
index 0bd96f27..3c93a4fa 100644
--- a/feature/logging/src/main/kotlin/com/f0x1d/feature/logging/ui/fragment/LogsExtendedCopyFragment.kt
+++ b/feature/logging/extended-copy/src/main/kotlin/com/f0x1d/logfox/feature/logging/extended/copy/presentation/ui/LogsExtendedCopyFragment.kt
@@ -1,20 +1,21 @@
-package com.f0x1d.feature.logging.ui.fragment
+package com.f0x1d.logfox.feature.logging.extended.copy.presentation.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import androidx.hilt.navigation.fragment.hiltNavGraphViewModels
-import com.f0x1d.feature.logging.viewmodel.LogsViewModel
-import com.f0x1d.logfox.arch.ui.fragment.BaseViewModelFragment
-import com.f0x1d.logfox.feature.logging.databinding.FragmentLogsExtendedCopyBinding
-import com.f0x1d.logfox.navigation.Directions
+import androidx.fragment.app.viewModels
+import com.f0x1d.logfox.arch.presentation.ui.fragment.BaseFragment
+import com.f0x1d.logfox.feature.logging.extended.copy.databinding.FragmentLogsExtendedCopyBinding
+import com.f0x1d.logfox.feature.logging.extended.copy.presentation.LogsExtendedCopyViewModel
import com.f0x1d.logfox.ui.view.setupBackButtonForNavController
+import dagger.hilt.android.AndroidEntryPoint
import dev.chrisbanes.insetter.applyInsetter
-class LogsExtendedCopyFragment: BaseViewModelFragment() {
+@AndroidEntryPoint
+class LogsExtendedCopyFragment : BaseFragment() {
- override val viewModel by hiltNavGraphViewModels(Directions.logsFragment)
+ private val viewModel by viewModels()
override fun inflateBinding(
inflater: LayoutInflater,
@@ -29,6 +30,8 @@ class LogsExtendedCopyFragment: BaseViewModelFragment
+ logText.text = state.text
+ }
}
}
diff --git a/feature/logging/src/main/res/layout/fragment_logs_extended_copy.xml b/feature/logging/extended-copy/src/main/res/layout/fragment_logs_extended_copy.xml
similarity index 100%
rename from feature/logging/src/main/res/layout/fragment_logs_extended_copy.xml
rename to feature/logging/extended-copy/src/main/res/layout/fragment_logs_extended_copy.xml
diff --git a/feature/crashes/.gitignore b/feature/logging/impl/.gitignore
similarity index 100%
rename from feature/crashes/.gitignore
rename to feature/logging/impl/.gitignore
diff --git a/feature/logging/impl/build.gradle.kts b/feature/logging/impl/build.gradle.kts
new file mode 100644
index 00000000..1bee1f41
--- /dev/null
+++ b/feature/logging/impl/build.gradle.kts
@@ -0,0 +1,9 @@
+plugins {
+ id("logfox.android.feature")
+}
+
+android.namespace = "com.f0x1d.logfox.feature.logging.impl"
+
+dependencies {
+ implementation(projects.feature.logging.api)
+}
diff --git a/feature/logging/impl/src/main/kotlin/com/f0x1d/logfox/feature/logging/impl/data/LoggingRepositoryImpl.kt b/feature/logging/impl/src/main/kotlin/com/f0x1d/logfox/feature/logging/impl/data/LoggingRepositoryImpl.kt
new file mode 100644
index 00000000..650a538c
--- /dev/null
+++ b/feature/logging/impl/src/main/kotlin/com/f0x1d/logfox/feature/logging/impl/data/LoggingRepositoryImpl.kt
@@ -0,0 +1,118 @@
+package com.f0x1d.logfox.feature.logging.impl.data
+
+import android.content.Context
+import com.f0x1d.logfox.arch.di.IODispatcher
+import com.f0x1d.logfox.feature.logging.api.data.LoggingRepository
+import com.f0x1d.logfox.model.exception.TerminalNotSupportedException
+import com.f0x1d.logfox.model.logline.LogLine
+import com.f0x1d.logfox.preferences.shared.AppPreferences
+import com.f0x1d.logfox.terminals.base.Terminal
+import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.channels.ProducerScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.channelFlow
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withTimeout
+import timber.log.Timber
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.seconds
+
+internal class LoggingRepositoryImpl @Inject constructor(
+ @ApplicationContext private val context: Context,
+ private val appPreferences: AppPreferences,
+ @IODispatcher private val ioDispatcher: CoroutineDispatcher,
+) : LoggingRepository {
+
+ override fun startLogging(
+ terminal: Terminal,
+ startingId: Long,
+ ): Flow = channelFlow {
+ val command = LoggingRepository.COMMAND + when (appPreferences.showLogsFromAppLaunch) {
+ true -> LoggingRepository.SHOW_LOGS_FROM_NOW_FLAGS
+
+ else -> emptyArray()
+ }
+
+ emitLines(
+ terminal = terminal,
+ command = command,
+ startingId = startingId,
+ )
+ }.flowOn(ioDispatcher)
+
+ override fun dumpLogs(terminal: Terminal): Flow = channelFlow {
+ val command = LoggingRepository.COMMAND + LoggingRepository.DUMP_FLAG
+
+ emitLines(
+ terminal = terminal,
+ command = command,
+ startingId = 0,
+ )
+ }.flowOn(ioDispatcher)
+
+ private suspend fun ProducerScope.emitLines(
+ terminal: Terminal,
+ command: Array,
+ startingId: Long = 0,
+ ) {
+ if (terminal.isSupported().not()) {
+ Timber.d("terminal $terminal is not supported")
+ throw TerminalNotSupportedException()
+ }
+
+ val process = terminal.execute(*command) ?: throw TerminalNotSupportedException()
+ Timber.d("started process")
+
+ var idsCounter = startingId
+ Timber.d("starting with id $idsCounter")
+
+ try {
+ val readerScope = CoroutineScope(ioDispatcher + SupervisorJob())
+ invokeOnClose { readerScope.cancel() }
+
+ process.output.bufferedReader().useLines { linesSequence ->
+ var droppedFirst = !appPreferences.showLogsFromAppLaunch
+ // avoiding getting the same line after logging restart because of
+ // WARNING: -T 0 invalid, setting to 1
+ val iterator = linesSequence.iterator()
+
+ var looping = true
+ while (looping) {
+ withTimeout(10.seconds) {
+ readerScope.launch {
+ if (iterator.hasNext().not()) {
+ looping = false
+ } else {
+ val line = iterator.next()
+ Timber.d("got line $line")
+
+ val logLine = LogLine(
+ id = idsCounter++,
+ line = line,
+ context = context,
+ )
+ Timber.d("successfully parsed $line to $logLine")
+
+ if (droppedFirst.not()) {
+ droppedFirst = true
+ } else {
+ logLine?.let { send(it) }
+ }
+ }
+ }.join()
+ }
+ }
+ }
+ } finally {
+ Timber.d("destroying process")
+ runCatching {
+ process.destroy()
+ }
+ }
+ }
+}
diff --git a/feature/logging-core/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/store/LoggingStore.kt b/feature/logging/impl/src/main/kotlin/com/f0x1d/logfox/feature/logging/impl/data/LogsDataSourceImpl.kt
similarity index 64%
rename from feature/logging-core/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/store/LoggingStore.kt
rename to feature/logging/impl/src/main/kotlin/com/f0x1d/logfox/feature/logging/impl/data/LogsDataSourceImpl.kt
index 92ba381b..5d472ab9 100644
--- a/feature/logging-core/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/store/LoggingStore.kt
+++ b/feature/logging/impl/src/main/kotlin/com/f0x1d/logfox/feature/logging/impl/data/LogsDataSourceImpl.kt
@@ -1,31 +1,25 @@
-package com.f0x1d.logfox.feature.logging.core.store
+package com.f0x1d.logfox.feature.logging.impl.data
import com.f0x1d.logfox.arch.di.DefaultDispatcher
+import com.f0x1d.logfox.feature.logging.api.data.LogsDataSource
import com.f0x1d.logfox.model.logline.LogLine
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton
-// I completely don't like it
-// I really want to get rid of Singletons in project
-interface LoggingStore {
- val logs: Flow>
-
- suspend fun updateLogs(logs: List)
-}
-
@Singleton
-internal class LoggingStoreImpl @Inject constructor(
+internal class LogsDataSourceImpl @Inject constructor(
@DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher,
-) : LoggingStore {
+) : LogsDataSource {
private val mutableLogs = MutableStateFlow(emptyList())
- override val logs: Flow> = mutableLogs
+ override val logs: Flow> get() = mutableLogs.asStateFlow()
override suspend fun updateLogs(logs: List) = withContext(defaultDispatcher) {
mutableLogs.update { logs.toMutableList() }
diff --git a/feature/logging/impl/src/main/kotlin/com/f0x1d/logfox/feature/logging/impl/data/QueryDataSourceImpl.kt b/feature/logging/impl/src/main/kotlin/com/f0x1d/logfox/feature/logging/impl/data/QueryDataSourceImpl.kt
new file mode 100644
index 00000000..ca44e16c
--- /dev/null
+++ b/feature/logging/impl/src/main/kotlin/com/f0x1d/logfox/feature/logging/impl/data/QueryDataSourceImpl.kt
@@ -0,0 +1,25 @@
+package com.f0x1d.logfox.feature.logging.impl.data
+
+import com.f0x1d.logfox.arch.di.DefaultDispatcher
+import com.f0x1d.logfox.feature.logging.api.data.QueryDataSource
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.withContext
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+internal class QueryDataSourceImpl @Inject constructor(
+ @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher,
+) : QueryDataSource {
+ private val mutableQuery = MutableStateFlow(null)
+
+ override val query: Flow get() = mutableQuery.asStateFlow()
+
+ override suspend fun updateQuery(query: String?) = withContext(defaultDispatcher) {
+ mutableQuery.update { query }
+ }
+}
diff --git a/feature/logging/impl/src/main/kotlin/com/f0x1d/logfox/feature/logging/impl/data/SelectedLogLinesDataSourceImpl.kt b/feature/logging/impl/src/main/kotlin/com/f0x1d/logfox/feature/logging/impl/data/SelectedLogLinesDataSourceImpl.kt
new file mode 100644
index 00000000..54fdf0de
--- /dev/null
+++ b/feature/logging/impl/src/main/kotlin/com/f0x1d/logfox/feature/logging/impl/data/SelectedLogLinesDataSourceImpl.kt
@@ -0,0 +1,26 @@
+package com.f0x1d.logfox.feature.logging.impl.data
+
+import com.f0x1d.logfox.arch.di.DefaultDispatcher
+import com.f0x1d.logfox.feature.logging.api.data.SelectedLogLinesDataSource
+import com.f0x1d.logfox.model.logline.LogLine
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.withContext
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+internal class SelectedLogLinesDataSourceImpl @Inject constructor(
+ @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher,
+) : SelectedLogLinesDataSource {
+ private val mutableLines = MutableStateFlow(emptyList())
+
+ override val selectedLines: Flow> get() = mutableLines.asStateFlow()
+
+ override suspend fun updateSelectedLines(selectedLines: List) = withContext(defaultDispatcher) {
+ mutableLines.update { selectedLines.toMutableList() }
+ }
+}
diff --git a/feature/logging/impl/src/main/kotlin/com/f0x1d/logfox/feature/logging/impl/di/DataSourcesModule.kt b/feature/logging/impl/src/main/kotlin/com/f0x1d/logfox/feature/logging/impl/di/DataSourcesModule.kt
new file mode 100644
index 00000000..b72a83b4
--- /dev/null
+++ b/feature/logging/impl/src/main/kotlin/com/f0x1d/logfox/feature/logging/impl/di/DataSourcesModule.kt
@@ -0,0 +1,32 @@
+package com.f0x1d.logfox.feature.logging.impl.di
+
+import com.f0x1d.logfox.feature.logging.api.data.LogsDataSource
+import com.f0x1d.logfox.feature.logging.api.data.QueryDataSource
+import com.f0x1d.logfox.feature.logging.api.data.SelectedLogLinesDataSource
+import com.f0x1d.logfox.feature.logging.impl.data.LogsDataSourceImpl
+import com.f0x1d.logfox.feature.logging.impl.data.QueryDataSourceImpl
+import com.f0x1d.logfox.feature.logging.impl.data.SelectedLogLinesDataSourceImpl
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+
+@Module
+@InstallIn(SingletonComponent::class)
+internal interface DataSourcesModule {
+
+ @Binds
+ fun bindLogsDataSource(
+ logsDataSourceImpl: LogsDataSourceImpl,
+ ): LogsDataSource
+
+ @Binds
+ fun bindQueryDataSource(
+ queryDataSourceImpl: QueryDataSourceImpl,
+ ): QueryDataSource
+
+ @Binds
+ fun bindSelectedLogLinesDataSource(
+ selectedLogLinesDataSourceImpl: SelectedLogLinesDataSourceImpl,
+ ): SelectedLogLinesDataSource
+}
diff --git a/feature/logging-core/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/di/RepositoriesModule.kt b/feature/logging/impl/src/main/kotlin/com/f0x1d/logfox/feature/logging/impl/di/RepositoriesModule.kt
similarity index 62%
rename from feature/logging-core/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/di/RepositoriesModule.kt
rename to feature/logging/impl/src/main/kotlin/com/f0x1d/logfox/feature/logging/impl/di/RepositoriesModule.kt
index 5b8d7760..a7b862e7 100644
--- a/feature/logging-core/src/main/kotlin/com/f0x1d/logfox/feature/logging/core/di/RepositoriesModule.kt
+++ b/feature/logging/impl/src/main/kotlin/com/f0x1d/logfox/feature/logging/impl/di/RepositoriesModule.kt
@@ -1,7 +1,7 @@
-package com.f0x1d.logfox.feature.logging.core.di
+package com.f0x1d.logfox.feature.logging.impl.di
-import com.f0x1d.logfox.feature.logging.core.repository.LoggingRepository
-import com.f0x1d.logfox.feature.logging.core.repository.LoggingRepositoryImpl
+import com.f0x1d.logfox.feature.logging.api.data.LoggingRepository
+import com.f0x1d.logfox.feature.logging.impl.data.LoggingRepositoryImpl
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
diff --git a/feature/recordings/.gitignore b/feature/logging/list/.gitignore
similarity index 100%
rename from feature/recordings/.gitignore
rename to feature/logging/list/.gitignore
diff --git a/feature/logging/list/build.gradle.kts b/feature/logging/list/build.gradle.kts
new file mode 100644
index 00000000..01b3df10
--- /dev/null
+++ b/feature/logging/list/build.gradle.kts
@@ -0,0 +1,12 @@
+plugins {
+ id("logfox.android.feature")
+}
+
+android.namespace = "com.f0x1d.logfox.feature.logging.list"
+
+dependencies {
+ implementation(projects.feature.crashes.api)
+ implementation(projects.feature.filters.api)
+ implementation(projects.feature.logging.api)
+ implementation(projects.feature.recordings.api)
+}
diff --git a/feature/logging/src/main/kotlin/com/f0x1d/feature/logging/di/LogsViewModelModule.kt b/feature/logging/list/src/main/kotlin/com/f0x1d/logfox/feature/logging/list/di/LogsViewModelModule.kt
similarity index 93%
rename from feature/logging/src/main/kotlin/com/f0x1d/feature/logging/di/LogsViewModelModule.kt
rename to feature/logging/list/src/main/kotlin/com/f0x1d/logfox/feature/logging/list/di/LogsViewModelModule.kt
index d286626e..898dd320 100644
--- a/feature/logging/src/main/kotlin/com/f0x1d/feature/logging/di/LogsViewModelModule.kt
+++ b/feature/logging/list/src/main/kotlin/com/f0x1d/logfox/feature/logging/list/di/LogsViewModelModule.kt
@@ -1,4 +1,4 @@
-package com.f0x1d.feature.logging.di
+package com.f0x1d.logfox.feature.logging.list.di
import android.content.Intent
import android.net.Uri
diff --git a/feature/logging/list/src/main/kotlin/com/f0x1d/logfox/feature/logging/list/presentation/LogsAction.kt b/feature/logging/list/src/main/kotlin/com/f0x1d/logfox/feature/logging/list/presentation/LogsAction.kt
new file mode 100644
index 00000000..461401eb
--- /dev/null
+++ b/feature/logging/list/src/main/kotlin/com/f0x1d/logfox/feature/logging/list/presentation/LogsAction.kt
@@ -0,0 +1,3 @@
+package com.f0x1d.logfox.feature.logging.list.presentation
+
+sealed interface LogsAction
diff --git a/feature/logging/list/src/main/kotlin/com/f0x1d/logfox/feature/logging/list/presentation/LogsState.kt b/feature/logging/list/src/main/kotlin/com/f0x1d/logfox/feature/logging/list/presentation/LogsState.kt
new file mode 100644
index 00000000..5737d374
--- /dev/null
+++ b/feature/logging/list/src/main/kotlin/com/f0x1d/logfox/feature/logging/list/presentation/LogsState.kt
@@ -0,0 +1,12 @@
+package com.f0x1d.logfox.feature.logging.list.presentation
+
+import com.f0x1d.logfox.database.entity.UserFilter
+import com.f0x1d.logfox.model.logline.LogLine
+
+data class LogsState(
+ val logs: List = emptyList(),
+ val paused: Boolean = false,
+ val query: String? = null,
+ val filters: List = emptyList(),
+ val selectedItems: Set = emptySet(),
+)
diff --git a/feature/logging/list/src/main/kotlin/com/f0x1d/logfox/feature/logging/list/presentation/LogsViewModel.kt b/feature/logging/list/src/main/kotlin/com/f0x1d/logfox/feature/logging/list/presentation/LogsViewModel.kt
new file mode 100644
index 00000000..fa812212
--- /dev/null
+++ b/feature/logging/list/src/main/kotlin/com/f0x1d/logfox/feature/logging/list/presentation/LogsViewModel.kt
@@ -0,0 +1,195 @@
+package com.f0x1d.logfox.feature.logging.list.presentation
+
+import android.app.Application
+import android.net.Uri
+import androidx.lifecycle.viewModelScope
+import com.f0x1d.logfox.arch.di.DefaultDispatcher
+import com.f0x1d.logfox.arch.di.IODispatcher
+import com.f0x1d.logfox.arch.viewmodel.BaseViewModel
+import com.f0x1d.logfox.database.entity.UserFilter
+import com.f0x1d.logfox.datetime.DateTimeFormatter
+import com.f0x1d.logfox.feature.filters.api.data.FiltersRepository
+import com.f0x1d.logfox.feature.logging.api.data.LogsDataSource
+import com.f0x1d.logfox.feature.logging.api.data.QueryDataSource
+import com.f0x1d.logfox.feature.logging.api.data.SelectedLogLinesDataSource
+import com.f0x1d.logfox.feature.logging.api.model.filterAndSearch
+import com.f0x1d.logfox.feature.logging.list.di.FileUri
+import com.f0x1d.logfox.feature.recordings.api.data.RecordingsRepository
+import com.f0x1d.logfox.model.logline.LogLine
+import com.f0x1d.logfox.preferences.shared.AppPreferences
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.scan
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import javax.inject.Inject
+
+@HiltViewModel
+class LogsViewModel @Inject constructor(
+ @FileUri val fileUri: Uri?,
+ private val logsDataSource: LogsDataSource,
+ private val queryDataSource: QueryDataSource,
+ private val selectedLogLinesDataSource: SelectedLogLinesDataSource,
+ private val filtersRepository: FiltersRepository,
+ private val recordingsRepository: RecordingsRepository,
+ private val appPreferences: AppPreferences,
+ @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher,
+ @IODispatcher private val ioDispatcher: CoroutineDispatcher,
+ dateTimeFormatter: DateTimeFormatter,
+ application: Application,
+): BaseViewModel(
+ initialStateProvider = { LogsState() },
+ application = application,
+), DateTimeFormatter by dateTimeFormatter {
+ val viewingFile = fileUri != null
+ val viewingFileName = fileUri?.readFileName(ctx)
+
+ val resumeLoggingWithBottomTouch get() = appPreferences.resumeLoggingWithBottomTouch
+ val logsTextSize get() = appPreferences.logsTextSize.toFloat()
+ val logsExpanded get() = appPreferences.logsExpanded
+ val logsFormat get() = appPreferences.showLogValues
+
+ val selectedItemsContent get() = currentState.selectedItems.joinToString("\n") {
+ originalOf(it)
+ }
+
+ init {
+ load()
+ }
+
+ private fun load() {
+ viewModelScope.launch {
+ state
+ .map { it.selectedItems }
+ .distinctUntilChanged()
+ .onEach { selectedLogLinesDataSource.updateSelectedLines(it.toList()) }
+ .launchIn(this)
+
+ combine(
+ fileUri?.readFileContentsAsFlow(
+ context = ctx,
+ logsDisplayLimit = appPreferences.logsDisplayLimit,
+ ) ?: logsDataSource.logs,
+ filtersRepository.getAllEnabledAsFlow(),
+ queryDataSource.query,
+ if (viewingFile.not()) {
+ state
+ .map { it.paused }
+ .distinctUntilChanged()
+ } else {
+ flowOf(false)
+ },
+ ) { logs, filters, query, paused ->
+ LogsData(
+ logs = logs,
+ filters = filters,
+ query = query,
+ paused = paused,
+ )
+ }.scan(LogsData()) { accumulator, data ->
+ when {
+ !data.paused
+ // In case they were cleared
+ || data.logs.isEmpty() -> data
+
+ data.query != accumulator.query
+ || data.filters != accumulator.filters
+ -> data.copy(
+ logs = accumulator.logs,
+ )
+
+ else -> data.copy(
+ logs = accumulator.logs,
+ passing = false,
+ )
+ }
+ }.filter { data ->
+ data.passing
+ }.mapNotNull { data ->
+ data.copy(
+ logs = data.logs.filterAndSearch(
+ filters = data.filters,
+ query = data.query,
+ )
+ )
+ }.flowOn(
+ defaultDispatcher,
+ ).onEach { data ->
+ reduce {
+ copy(
+ logs = data.logs,
+ query = data.query,
+ filters = data.filters,
+ )
+ }
+ }.launchIn(this)
+ }
+ }
+
+ fun selectLine(logLine: LogLine, selected: Boolean) = reduce {
+ copy(
+ selectedItems = selectedItems.toMutableSet().apply {
+ if (selected) add(
+ logLine
+ ) else remove(
+ logLine
+ )
+ }
+ )
+ }
+
+ fun selectAll() = reduce {
+ copy(
+ selectedItems = if (selectedItems.containsAll(logs)) {
+ emptySet()
+ } else {
+ logs.toSet()
+ },
+ )
+ }
+
+ fun selectedToRecording() = launchCatching {
+ recordingsRepository.createRecordingFrom(
+ lines = withContext(defaultDispatcher) {
+ currentState.selectedItems.sortedBy { it.dateAndTime }
+ },
+ )
+ }
+
+ fun exportSelectedLogsTo(uri: Uri) = launchCatching(ioDispatcher) {
+ ctx.contentResolver.openOutputStream(uri)?.use {
+ it.write(selectedItemsContent.encodeToByteArray())
+ }
+ }
+
+ fun switchState() = reduce { copy(paused = paused.not()) }
+ fun pause() = reduce { copy(paused = true) }
+ fun resume() = reduce { copy(paused = false) }
+
+ fun originalOf(logLine: LogLine): String = appPreferences.originalOf(
+ logLine = logLine,
+ formatDate = ::formatDate,
+ formatTime = ::formatTime,
+ )
+
+ fun clearSelection() = reduce {
+ copy(selectedItems = emptySet())
+ }
+
+ private data class LogsData(
+ val logs: List = emptyList(),
+ val filters: List = emptyList(),
+ val query: String? = null,
+ val paused: Boolean = false,
+ val passing: Boolean = true,
+ )
+}
diff --git a/feature/logging/src/main/kotlin/com/f0x1d/feature/logging/viewmodel/UriExt.kt b/feature/logging/list/src/main/kotlin/com/f0x1d/logfox/feature/logging/list/presentation/UriExt.kt
similarity index 77%
rename from feature/logging/src/main/kotlin/com/f0x1d/feature/logging/viewmodel/UriExt.kt
rename to feature/logging/list/src/main/kotlin/com/f0x1d/logfox/feature/logging/list/presentation/UriExt.kt
index e770ac14..d81af537 100644
--- a/feature/logging/src/main/kotlin/com/f0x1d/feature/logging/viewmodel/UriExt.kt
+++ b/feature/logging/list/src/main/kotlin/com/f0x1d/logfox/feature/logging/list/presentation/UriExt.kt
@@ -1,8 +1,9 @@
-package com.f0x1d.feature.logging.viewmodel
+package com.f0x1d.logfox.feature.logging.list.presentation
import android.content.Context
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
+import com.f0x1d.logfox.model.logline.LogLevel
import com.f0x1d.logfox.model.logline.LogLine
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flow
@@ -26,6 +27,13 @@ internal fun Uri?.readFileContentsAsFlow(
for (line in lines) {
val logLine = LogLine(id, line, context) ?: LogLine(
id = id,
+ dateAndTime = System.currentTimeMillis(),
+ uid = "",
+ pid = "",
+ tid = "",
+ packageName = null,
+ level = LogLevel.INFO,
+ tag = "",
content = line,
originalContent = line,
)
diff --git a/feature/logging/src/main/kotlin/com/f0x1d/feature/logging/adapter/LogsAdapter.kt b/feature/logging/list/src/main/kotlin/com/f0x1d/logfox/feature/logging/list/presentation/adapter/LogsAdapter.kt
similarity index 79%
rename from feature/logging/src/main/kotlin/com/f0x1d/feature/logging/adapter/LogsAdapter.kt
rename to feature/logging/list/src/main/kotlin/com/f0x1d/logfox/feature/logging/list/presentation/adapter/LogsAdapter.kt
index 4c4a68d1..c60e7ac1 100644
--- a/feature/logging/src/main/kotlin/com/f0x1d/feature/logging/adapter/LogsAdapter.kt
+++ b/feature/logging/list/src/main/kotlin/com/f0x1d/logfox/feature/logging/list/presentation/adapter/LogsAdapter.kt
@@ -1,10 +1,10 @@
-package com.f0x1d.feature.logging.adapter
+package com.f0x1d.logfox.feature.logging.list.presentation.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
-import com.f0x1d.feature.logging.ui.viewholder.LogViewHolder
-import com.f0x1d.logfox.arch.adapter.BaseListAdapter
-import com.f0x1d.logfox.feature.logging.databinding.ItemLogBinding
+import com.f0x1d.logfox.arch.presentation.adapter.BaseListAdapter
+import com.f0x1d.logfox.feature.logging.list.databinding.ItemLogBinding
+import com.f0x1d.logfox.feature.logging.list.presentation.ui.viewholder.LogViewHolder
import com.f0x1d.logfox.model.diffCallback
import com.f0x1d.logfox.model.logline.LogLine
import com.f0x1d.logfox.model.preferences.ShowLogValues
diff --git a/feature/logging/src/main/kotlin/com/f0x1d/feature/logging/ui/fragment/LogsFragment.kt b/feature/logging/list/src/main/kotlin/com/f0x1d/logfox/feature/logging/list/presentation/ui/fragment/LogsFragment.kt
similarity index 64%
rename from feature/logging/src/main/kotlin/com/f0x1d/feature/logging/ui/fragment/LogsFragment.kt
rename to feature/logging/list/src/main/kotlin/com/f0x1d/logfox/feature/logging/list/presentation/ui/fragment/LogsFragment.kt
index b49f0570..ca212df9 100644
--- a/feature/logging/src/main/kotlin/com/f0x1d/feature/logging/ui/fragment/LogsFragment.kt
+++ b/feature/logging/list/src/main/kotlin/com/f0x1d/logfox/feature/logging/list/presentation/ui/fragment/LogsFragment.kt
@@ -1,4 +1,4 @@
-package com.f0x1d.feature.logging.ui.fragment
+package com.f0x1d.logfox.feature.logging.list.presentation.ui.fragment
import android.os.Bundle
import android.view.LayoutInflater
@@ -6,20 +6,19 @@ import android.view.View
import android.view.ViewGroup
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts
-import androidx.core.os.bundleOf
-import androidx.hilt.navigation.fragment.hiltNavGraphViewModels
+import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
-import com.f0x1d.feature.logging.adapter.LogsAdapter
-import com.f0x1d.feature.logging.service.LoggingService
-import com.f0x1d.feature.logging.viewmodel.LogsViewModel
import com.f0x1d.logfox.arch.copyText
import com.f0x1d.logfox.arch.isHorizontalOrientation
-import com.f0x1d.logfox.arch.sendService
-import com.f0x1d.logfox.arch.ui.fragment.BaseViewModelFragment
-import com.f0x1d.logfox.feature.logging.R
-import com.f0x1d.logfox.feature.logging.databinding.FragmentLogsBinding
+import com.f0x1d.logfox.arch.presentation.ui.fragment.BaseFragment
+import com.f0x1d.logfox.database.entity.UserFilter
+import com.f0x1d.logfox.feature.logging.api.presentation.LoggingServiceDelegate
+import com.f0x1d.logfox.feature.logging.list.R
+import com.f0x1d.logfox.feature.logging.list.databinding.FragmentLogsBinding
+import com.f0x1d.logfox.feature.logging.list.presentation.LogsViewModel
+import com.f0x1d.logfox.feature.logging.list.presentation.adapter.LogsAdapter
import com.f0x1d.logfox.model.logline.LogLine
import com.f0x1d.logfox.navigation.Directions
import com.f0x1d.logfox.strings.Plurals
@@ -31,12 +30,15 @@ import com.f0x1d.logfox.ui.view.setupBackButtonForNavController
import com.f0x1d.logfox.ui.view.setupCloseButton
import dagger.hilt.android.AndroidEntryPoint
import dev.chrisbanes.insetter.applyInsetter
-import kotlinx.coroutines.flow.update
+import javax.inject.Inject
@AndroidEntryPoint
-class LogsFragment: BaseViewModelFragment() {
+class LogsFragment : BaseFragment() {
- override val viewModel by hiltNavGraphViewModels(Directions.logsFragment)
+ private val viewModel by viewModels()
+
+ @Inject
+ lateinit var loggingServiceDelegate: LoggingServiceDelegate
private val adapter by lazy {
LogsAdapter(
@@ -52,22 +54,21 @@ class LogsFragment: BaseViewModelFragment()
},
)
}
- private var changingState = false
private val clearSelectionOnBackPressedCallback = object : OnBackPressedCallback(false) {
override fun handleOnBackPressed() {
- viewModel.selectedItems.update {
- emptySet()
- }
+ viewModel.clearSelection()
}
}
private val exportLogsLauncher = registerForActivityResult(
- ActivityResultContracts.CreateDocument("text/*")
+ ActivityResultContracts.CreateDocument("text/*"),
) {
viewModel.exportSelectedLogsTo(it ?: return@registerForActivityResult)
}
+ private var lastPauseEventTimeMillis = 0L
+
override fun inflateBinding(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -105,12 +106,7 @@ class LogsFragment: BaseViewModelFragment()
snackbar(Strings.text_copied)
}
setClickListenerOn(R.id.extended_copy_selected_item) {
- findNavController().navigate(
- resId = Directions.action_logsFragment_to_logsExtendedCopyFragment,
- args = bundleOf(
- "content" to viewModel.selectedItemsContent,
- ),
- )
+ findNavController().navigate(Directions.action_logsFragment_to_logsExtendedCopyFragment)
}
setClickListenerOn(R.id.selected_to_recording_item) {
viewModel.selectedToRecording()
@@ -122,13 +118,13 @@ class LogsFragment: BaseViewModelFragment()
)
}
setClickListenerOn(R.id.clear_item) {
- requireContext().sendService(action = LoggingService.ACTION_CLEAR_LOGS)
+ loggingServiceDelegate.clearLogs()
}
setClickListenerOn(R.id.restart_logging_item) {
- requireContext().sendService(action = LoggingService.ACTION_RESTART_LOGGING)
+ loggingServiceDelegate.restartLogging()
}
setClickListenerOn(R.id.exit_item) {
- requireContext().sendService(action = LoggingService.ACTION_KILL_SERVICE)
+ loggingServiceDelegate.killService()
}
}
@@ -136,17 +132,19 @@ class LogsFragment: BaseViewModelFragment()
logsRecycler.itemAnimator = null
logsRecycler.recycledViewPool.setMaxRecycledViews(0, 50)
logsRecycler.adapter = adapter
- logsRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
- override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
- if (changingState)
- return
-
- if (viewModel.paused.value && !recyclerView.canScrollVertically(1)) {
- if (viewModel.resumeLoggingWithBottomTouch) viewModel.resume()
- } else
- viewModel.pause()
- }
- })
+ logsRecycler.addOnScrollListener(
+ object : RecyclerView.OnScrollListener() {
+ override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
+ if (viewModel.currentState.paused && !recyclerView.canScrollVertically(1)) {
+ val enoughTimePassed = (System.currentTimeMillis() - lastPauseEventTimeMillis) > 300
+ if (viewModel.resumeLoggingWithBottomTouch && enoughTimePassed) viewModel.resume()
+ } else {
+ lastPauseEventTimeMillis = System.currentTimeMillis()
+ viewModel.pause()
+ }
+ }
+ },
+ )
scrollFab.setOnClickListener {
if (viewModel.resumeLoggingWithBottomTouch)
@@ -155,65 +153,66 @@ class LogsFragment: BaseViewModelFragment()
scrollLogToBottom()
}
- viewModel.queryAndFilters.collectWithLifecycle {
- val (query, filters) = it
+ viewModel.state.collectWithLifecycle { state ->
+ processQueryAndFilters(
+ query = state.query,
+ filters = state.filters,
+ )
+ processSelectedItems(selectedItems = state.selectedItems)
+ processPaused(paused = state.paused)
- val subtitle = buildString {
- if (query != null) {
- append(query)
+ updateLogsList(items = state.logs)
+ }
- if (filters.isNotEmpty())
- append(", ")
- }
+ requireActivity().onBackPressedDispatcher.apply {
+ addCallback(viewLifecycleOwner, clearSelectionOnBackPressedCallback)
+ }
+ }
+
+ private fun FragmentLogsBinding.processQueryAndFilters(query: String?, filters: List) {
+ val subtitle = buildString {
+ if (query != null) {
+ append(query)
if (filters.isNotEmpty())
- append(resources.getQuantityString(Plurals.filters_count, filters.size, filters.size))
+ append(", ")
}
- toolbar.subtitle = subtitle
- placeholderLayout.placeholderText.setText(
- when {
- viewModel.viewingFile -> Strings.no_logs
-
- subtitle.isEmpty() -> Strings.waiting_for_logs
-
- else -> Strings.all_logs_were_filtered_out
- }
- )
+ if (filters.isNotEmpty())
+ append(resources.getQuantityString(Plurals.filters_count, filters.size, filters.size))
}
- viewModel.selectedItems.collectWithLifecycle {
- val selecting = it.isNotEmpty()
-
- clearSelectionOnBackPressedCallback.isEnabled = selecting
+ toolbar.subtitle = subtitle
+ placeholderLayout.placeholderText.setText(
+ when {
+ viewModel.viewingFile -> Strings.no_logs
- adapter.selectedItems = it
- setupToolbarForSelection(selecting, it.size)
- }
+ subtitle.isEmpty() -> Strings.waiting_for_logs
- viewModel.logs.collectWithLifecycle {
- updateLogsList(it)
- }
+ else -> Strings.all_logs_were_filtered_out
+ }
+ )
+ }
- viewModel.paused.collectWithLifecycle { paused ->
- changingState = true
+ private fun FragmentLogsBinding.processSelectedItems(selectedItems: Set) {
+ val selecting = selectedItems.isNotEmpty()
- toolbar.menu.findItem(R.id.pause_item)
- .setIcon(if (paused) Icons.ic_play else Icons.ic_pause)
- .setTitle(if (paused) Strings.resume else Strings.pause)
+ clearSelectionOnBackPressedCallback.isEnabled = selecting
- if (paused) {
- scrollFab.show()
- } else {
- scrollFab.hide()
- scrollLogToBottom()
- }
+ adapter.selectedItems = selectedItems
+ setupToolbarForSelection(selecting, selectedItems.size)
+ }
- changingState = false
- }
+ private fun FragmentLogsBinding.processPaused(paused: Boolean) {
+ toolbar.menu.findItem(R.id.pause_item)
+ .setIcon(if (paused) Icons.ic_play else Icons.ic_pause)
+ .setTitle(if (paused) Strings.resume else Strings.pause)
- requireActivity().onBackPressedDispatcher.apply {
- addCallback(viewLifecycleOwner, clearSelectionOnBackPressedCallback)
+ if (paused) {
+ scrollFab.show()
+ } else {
+ scrollFab.hide()
+ scrollLogToBottom()
}
}
@@ -244,14 +243,13 @@ class LogsFragment: BaseViewModelFragment()
setupCloseButton()
setNavigationOnClickListener {
- viewModel.selectedItems.update {
- emptySet()
- }
+ viewModel.clearSelection()
}
- } else if (viewModel.viewingFile)
+ } else if (viewModel.viewingFile) {
setupBackButtonForNavController()
-
- else invalidateNavigationButton()
+ } else {
+ invalidateNavigationButton()
+ }
}
private fun FragmentLogsBinding.updateLogsList(items: List?) {
diff --git a/feature/logging/src/main/kotlin/com/f0x1d/feature/logging/ui/viewholder/LogViewHolder.kt b/feature/logging/list/src/main/kotlin/com/f0x1d/logfox/feature/logging/list/presentation/ui/viewholder/LogViewHolder.kt
similarity index 71%
rename from feature/logging/src/main/kotlin/com/f0x1d/feature/logging/ui/viewholder/LogViewHolder.kt
rename to feature/logging/list/src/main/kotlin/com/f0x1d/logfox/feature/logging/list/presentation/ui/viewholder/LogViewHolder.kt
index 0e5e3fbd..f70d3049 100644
--- a/feature/logging/src/main/kotlin/com/f0x1d/feature/logging/ui/viewholder/LogViewHolder.kt
+++ b/feature/logging/list/src/main/kotlin/com/f0x1d/logfox/feature/logging/list/presentation/ui/viewholder/LogViewHolder.kt
@@ -1,12 +1,11 @@
-package com.f0x1d.feature.logging.ui.viewholder
+package com.f0x1d.logfox.feature.logging.list.presentation.ui.viewholder
import android.view.Gravity
import androidx.appcompat.widget.PopupMenu
-import com.f0x1d.feature.logging.adapter.LogsAdapter
-import com.f0x1d.logfox.arch.ui.viewholder.BaseViewHolder
+import com.f0x1d.logfox.arch.presentation.ui.viewholder.BaseViewHolder
import com.f0x1d.logfox.datetime.dateTimeFormatter
-import com.f0x1d.logfox.feature.logging.R
-import com.f0x1d.logfox.feature.logging.databinding.ItemLogBinding
+import com.f0x1d.logfox.feature.logging.list.R
+import com.f0x1d.logfox.feature.logging.list.databinding.ItemLogBinding
import com.f0x1d.logfox.model.logline.LogLine
class LogViewHolder(
@@ -40,7 +39,7 @@ class LogViewHolder(
init {
binding.apply {
root.setOnClickListener {
- val adapter = adapter() ?: return@setOnClickListener
+ val adapter = adapter() ?: return@setOnClickListener
if (adapter.selectedItems.isNotEmpty())
selectItem()
@@ -48,7 +47,7 @@ class LogViewHolder(
expandOrCollapseItem()
}
root.setOnLongClickListener {
- val adapter = adapter() ?: return@setOnLongClickListener true
+ val adapter = adapter() ?: return@setOnLongClickListener true
if (adapter.selectedItems.isNotEmpty())
expandOrCollapseItem()
@@ -61,12 +60,12 @@ class LogViewHolder(
}
override fun ItemLogBinding.bindTo(data: LogLine) {
- adapter()?.textSize?.also {
+ adapter()?.textSize?.also {
logText.textSize = it
levelView.textSize = it
}
- adapter()?.logsFormat?.let { values ->
+ adapter()?.logsFormat?.let { values ->
logText.text = data.formatOriginal(
values = values,
formatDate = dateTimeFormatter::formatDate,
@@ -83,14 +82,14 @@ class LogViewHolder(
popupMenu.dismiss()
}
- private fun selectItem() = adapter()?.selectedItems?.apply {
+ private fun selectItem() = adapter()?.selectedItems?.apply {
currentItem?.also {
val newValue = any { logLine -> it.id == logLine.id }.not()
selectedItem(it, newValue)
}
}
- private fun ItemLogBinding.expandOrCollapseItem() = adapter()?.apply {
+ private fun ItemLogBinding.expandOrCollapseItem() = adapter()?.apply {
expandedStates.apply {
currentItem?.also {
val newValue = getOrElse(it.id) { logsExpanded }.not()
@@ -101,7 +100,7 @@ class LogViewHolder(
}
}
- private fun ItemLogBinding.changeExpandedAndSelected(logLine: LogLine) = adapter()?.apply {
+ private fun ItemLogBinding.changeExpandedAndSelected(logLine: LogLine) = adapter()?.apply {
val expanded = expandedStates.getOrElse(logLine.id) { logsExpanded }
logText.maxLines = if (expanded) Int.MAX_VALUE else 1
diff --git a/feature/logging/src/main/res/layout/fragment_logs.xml b/feature/logging/list/src/main/res/layout/fragment_logs.xml
similarity index 100%
rename from feature/logging/src/main/res/layout/fragment_logs.xml
rename to feature/logging/list/src/main/res/layout/fragment_logs.xml
diff --git a/feature/logging/src/main/res/layout/item_log.xml b/feature/logging/list/src/main/res/layout/item_log.xml
similarity index 100%
rename from feature/logging/src/main/res/layout/item_log.xml
rename to feature/logging/list/src/main/res/layout/item_log.xml
diff --git a/feature/logging/src/main/res/layout/placeholder_logs.xml b/feature/logging/list/src/main/res/layout/placeholder_logs.xml
similarity index 100%
rename from feature/logging/src/main/res/layout/placeholder_logs.xml
rename to feature/logging/list/src/main/res/layout/placeholder_logs.xml
diff --git a/feature/logging/src/main/res/menu/log_menu.xml b/feature/logging/list/src/main/res/menu/log_menu.xml
similarity index 100%
rename from feature/logging/src/main/res/menu/log_menu.xml
rename to feature/logging/list/src/main/res/menu/log_menu.xml
diff --git a/feature/logging/src/main/res/menu/logs_menu.xml b/feature/logging/list/src/main/res/menu/logs_menu.xml
similarity index 100%
rename from feature/logging/src/main/res/menu/logs_menu.xml
rename to feature/logging/list/src/main/res/menu/logs_menu.xml
diff --git a/feature/logging/search/.gitignore b/feature/logging/search/.gitignore
new file mode 100644
index 00000000..567609b1
--- /dev/null
+++ b/feature/logging/search/.gitignore
@@ -0,0 +1 @@
+build/
diff --git a/feature/logging/search/build.gradle.kts b/feature/logging/search/build.gradle.kts
new file mode 100644
index 00000000..4e841a7d
--- /dev/null
+++ b/feature/logging/search/build.gradle.kts
@@ -0,0 +1,9 @@
+plugins {
+ id("logfox.android.feature")
+}
+
+android.namespace = "com.f0x1d.logfox.feature.logging.search"
+
+dependencies {
+ implementation(projects.feature.logging.api)
+}
diff --git a/feature/logging/search/src/main/kotlin/com/f0x1d/logfox/feature/logging/search/presentation/SearchLogsAction.kt b/feature/logging/search/src/main/kotlin/com/f0x1d/logfox/feature/logging/search/presentation/SearchLogsAction.kt
new file mode 100644
index 00000000..a63c8f46
--- /dev/null
+++ b/feature/logging/search/src/main/kotlin/com/f0x1d/logfox/feature/logging/search/presentation/SearchLogsAction.kt
@@ -0,0 +1,5 @@
+package com.f0x1d.logfox.feature.logging.search.presentation
+
+sealed interface SearchLogsAction {
+ data object Dismiss : SearchLogsAction
+}
diff --git a/feature/logging/search/src/main/kotlin/com/f0x1d/logfox/feature/logging/search/presentation/SearchLogsState.kt b/feature/logging/search/src/main/kotlin/com/f0x1d/logfox/feature/logging/search/presentation/SearchLogsState.kt
new file mode 100644
index 00000000..0f5949eb
--- /dev/null
+++ b/feature/logging/search/src/main/kotlin/com/f0x1d/logfox/feature/logging/search/presentation/SearchLogsState.kt
@@ -0,0 +1,5 @@
+package com.f0x1d.logfox.feature.logging.search.presentation
+
+data class SearchLogsState(
+ val query: String? = null,
+)
diff --git a/feature/logging/search/src/main/kotlin/com/f0x1d/logfox/feature/logging/search/presentation/SearchLogsViewModel.kt b/feature/logging/search/src/main/kotlin/com/f0x1d/logfox/feature/logging/search/presentation/SearchLogsViewModel.kt
new file mode 100644
index 00000000..9a32a19d
--- /dev/null
+++ b/feature/logging/search/src/main/kotlin/com/f0x1d/logfox/feature/logging/search/presentation/SearchLogsViewModel.kt
@@ -0,0 +1,37 @@
+package com.f0x1d.logfox.feature.logging.search.presentation
+
+import android.app.Application
+import androidx.lifecycle.viewModelScope
+import com.f0x1d.logfox.arch.viewmodel.BaseViewModel
+import com.f0x1d.logfox.feature.logging.api.data.QueryDataSource
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class SearchLogsViewModel @Inject constructor(
+ private val queryDataSource: QueryDataSource,
+ application: Application,
+) : BaseViewModel(
+ initialStateProvider = { SearchLogsState() },
+ application = application,
+) {
+ init {
+ load()
+ }
+
+ private fun load() {
+ viewModelScope.launch {
+ queryDataSource.query.collect { query ->
+ reduce { copy(query = query) }
+ }
+ }
+ }
+
+ fun updateQuery(query: String?) {
+ viewModelScope.launch {
+ queryDataSource.updateQuery(query)
+ sendAction(SearchLogsAction.Dismiss)
+ }
+ }
+}
diff --git a/feature/logging/src/main/kotlin/com/f0x1d/feature/logging/ui/dialog/SearchBottomSheet.kt b/feature/logging/search/src/main/kotlin/com/f0x1d/logfox/feature/logging/search/presentation/ui/SearchLogsBottomSheetFragment.kt
similarity index 50%
rename from feature/logging/src/main/kotlin/com/f0x1d/feature/logging/ui/dialog/SearchBottomSheet.kt
rename to feature/logging/search/src/main/kotlin/com/f0x1d/logfox/feature/logging/search/presentation/ui/SearchLogsBottomSheetFragment.kt
index f02ed602..3d3284bf 100644
--- a/feature/logging/src/main/kotlin/com/f0x1d/feature/logging/ui/dialog/SearchBottomSheet.kt
+++ b/feature/logging/search/src/main/kotlin/com/f0x1d/logfox/feature/logging/search/presentation/ui/SearchLogsBottomSheetFragment.kt
@@ -1,4 +1,4 @@
-package com.f0x1d.feature.logging.ui.dialog
+package com.f0x1d.logfox.feature.logging.search.presentation.ui
import android.os.Bundle
import android.view.LayoutInflater
@@ -6,15 +6,17 @@ import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import androidx.core.view.isVisible
-import androidx.hilt.navigation.fragment.hiltNavGraphViewModels
-import com.f0x1d.feature.logging.viewmodel.LogsViewModel
-import com.f0x1d.logfox.arch.ui.dialog.BaseBottomSheet
-import com.f0x1d.logfox.feature.logging.databinding.SheetSearchBinding
-import com.f0x1d.logfox.navigation.Directions
+import androidx.fragment.app.viewModels
+import com.f0x1d.logfox.arch.presentation.ui.dialog.BaseBottomSheetFragment
+import com.f0x1d.logfox.feature.logging.search.databinding.SheetSearchBinding
+import com.f0x1d.logfox.feature.logging.search.presentation.SearchLogsAction
+import com.f0x1d.logfox.feature.logging.search.presentation.SearchLogsViewModel
+import dagger.hilt.android.AndroidEntryPoint
-class SearchBottomSheet: BaseBottomSheet() {
+@AndroidEntryPoint
+class SearchLogsBottomSheetFragment : BaseBottomSheetFragment() {
- private val logsViewModel by hiltNavGraphViewModels(Directions.logsFragment)
+ private val viewModel by viewModels()
override fun inflateBinding(
inflater: LayoutInflater,
@@ -22,11 +24,6 @@ class SearchBottomSheet: BaseBottomSheet() {
) = SheetSearchBinding.inflate(inflater, container, false)
override fun SheetSearchBinding.onViewCreated(view: View, savedInstanceState: Bundle?) {
- val query = logsViewModel.query.value
-
- queryText.setText(query)
-
- clearSearchButton.isVisible = query != null
clearSearchButton.setOnClickListener {
search(null)
}
@@ -43,12 +40,23 @@ class SearchBottomSheet: BaseBottomSheet() {
}
queryText.requestFocus()
+
+ viewModel.state.collectWithLifecycle { state ->
+ queryText.setText(state.query)
+
+ clearSearchButton.isVisible = state.query != null
+ }
+
+ viewModel.actions.collectWithLifecycle { action ->
+ when (action) {
+ is SearchLogsAction.Dismiss -> dismiss()
+ }
+ }
}
private fun search(text: String?) {
if (text?.isEmpty() == true) return
- logsViewModel.query(text)
- dismiss()
+ viewModel.updateQuery(text)
}
}
diff --git a/feature/logging/src/main/res/layout/sheet_search.xml b/feature/logging/search/src/main/res/layout/sheet_search.xml
similarity index 100%
rename from feature/logging/src/main/res/layout/sheet_search.xml
rename to feature/logging/search/src/main/res/layout/sheet_search.xml
diff --git a/feature/logging/service/.gitignore b/feature/logging/service/.gitignore
new file mode 100644
index 00000000..567609b1
--- /dev/null
+++ b/feature/logging/service/.gitignore
@@ -0,0 +1 @@
+build/
diff --git a/feature/logging/service/build.gradle.kts b/feature/logging/service/build.gradle.kts
new file mode 100644
index 00000000..1ca94a83
--- /dev/null
+++ b/feature/logging/service/build.gradle.kts
@@ -0,0 +1,12 @@
+plugins {
+ id("logfox.android.feature")
+}
+
+android.namespace = "com.f0x1d.logfox.feature.logging.service"
+
+dependencies {
+ implementation(projects.feature.crashes.api)
+ implementation(projects.feature.filters.api)
+ implementation(projects.feature.logging.api)
+ implementation(projects.feature.recordings.api)
+}
diff --git a/feature/logging/src/main/AndroidManifest.xml b/feature/logging/service/src/main/AndroidManifest.xml
similarity index 85%
rename from feature/logging/src/main/AndroidManifest.xml
rename to feature/logging/service/src/main/AndroidManifest.xml
index 5a395514..fe396558 100644
--- a/feature/logging/src/main/AndroidManifest.xml
+++ b/feature/logging/service/src/main/AndroidManifest.xml
@@ -6,7 +6,7 @@
diff --git a/feature/logging/service/src/main/kotlin/com/f0x1d/logfox/feature/logging/service/di/LoggingServiceDelegateModule.kt b/feature/logging/service/src/main/kotlin/com/f0x1d/logfox/feature/logging/service/di/LoggingServiceDelegateModule.kt
new file mode 100644
index 00000000..ce82eb9e
--- /dev/null
+++ b/feature/logging/service/src/main/kotlin/com/f0x1d/logfox/feature/logging/service/di/LoggingServiceDelegateModule.kt
@@ -0,0 +1,18 @@
+package com.f0x1d.logfox.feature.logging.service.di
+
+import com.f0x1d.logfox.feature.logging.api.presentation.LoggingServiceDelegate
+import com.f0x1d.logfox.feature.logging.service.presentation.LoggingServiceDelegateImpl
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+
+@Module
+@InstallIn(SingletonComponent::class)
+internal interface LoggingServiceDelegateModule {
+
+ @Binds
+ fun bindLoggingServiceDelegate(
+ loggingServiceDelegateImpl: LoggingServiceDelegateImpl,
+ ): LoggingServiceDelegate
+}
diff --git a/feature/logging/src/main/kotlin/com/f0x1d/feature/logging/service/LoggingService.kt b/feature/logging/service/src/main/kotlin/com/f0x1d/logfox/feature/logging/service/presentation/LoggingService.kt
similarity index 84%
rename from feature/logging/src/main/kotlin/com/f0x1d/feature/logging/service/LoggingService.kt
rename to feature/logging/service/src/main/kotlin/com/f0x1d/logfox/feature/logging/service/presentation/LoggingService.kt
index fb0ef88f..f30bef78 100644
--- a/feature/logging/src/main/kotlin/com/f0x1d/feature/logging/service/LoggingService.kt
+++ b/feature/logging/service/src/main/kotlin/com/f0x1d/logfox/feature/logging/service/presentation/LoggingService.kt
@@ -1,4 +1,4 @@
-package com.f0x1d.feature.logging.service
+package com.f0x1d.logfox.feature.logging.service.presentation
import android.content.Intent
import android.os.Binder
@@ -15,12 +15,12 @@ import com.f0x1d.logfox.arch.di.DefaultDispatcher
import com.f0x1d.logfox.arch.makeServicePendingIntent
import com.f0x1d.logfox.arch.toast
import com.f0x1d.logfox.database.entity.UserFilter
-import com.f0x1d.logfox.feature.crashes.core.controller.CrashesController
-import com.f0x1d.logfox.feature.filters.core.repository.FiltersRepository
-import com.f0x1d.logfox.feature.logging.core.model.suits
-import com.f0x1d.logfox.feature.logging.core.repository.LoggingRepository
-import com.f0x1d.logfox.feature.logging.core.store.LoggingStore
-import com.f0x1d.logfox.feature.recordings.core.controller.RecordingController
+import com.f0x1d.logfox.feature.crashes.api.data.CrashesController
+import com.f0x1d.logfox.feature.filters.api.data.FiltersRepository
+import com.f0x1d.logfox.feature.logging.api.data.LoggingRepository
+import com.f0x1d.logfox.feature.logging.api.data.LogsDataSource
+import com.f0x1d.logfox.feature.logging.api.model.suits
+import com.f0x1d.logfox.feature.recordings.api.data.RecordingController
import com.f0x1d.logfox.model.exception.TerminalNotSupportedException
import com.f0x1d.logfox.model.logline.LogLine
import com.f0x1d.logfox.preferences.shared.AppPreferences
@@ -29,6 +29,7 @@ import com.f0x1d.logfox.terminals.DefaultTerminal
import com.f0x1d.logfox.terminals.base.Terminal
import com.f0x1d.logfox.ui.Icons
import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
@@ -42,6 +43,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
+import timber.log.Timber
import java.util.LinkedList
import javax.inject.Inject
@@ -70,7 +72,7 @@ class LoggingService : LifecycleService() {
lateinit var filtersRepository: FiltersRepository
@Inject
- lateinit var loggingStore: LoggingStore
+ lateinit var logsDataSource: LogsDataSource
@Inject
lateinit var appPreferences: AppPreferences
@@ -109,6 +111,7 @@ class LoggingService : LifecycleService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
+ Timber.d("got command ${intent?.action}")
when (intent?.action) {
ACTION_RESTART_LOGGING -> restartLogging()
ACTION_CLEAR_LOGS -> clearLogs()
@@ -120,10 +123,13 @@ class LoggingService : LifecycleService() {
}
private fun startLogging() {
+ Timber.d("startLogging")
if (loggingJob?.isActive == true) return
var loggingTerminal = terminals[appPreferences.selectedTerminalIndex]
+ Timber.d("selected terminal $loggingTerminal")
+
loggingJob = lifecycleScope.launch {
try {
launch {
@@ -131,16 +137,21 @@ class LoggingService : LifecycleService() {
delay(appPreferences.logsUpdateInterval)
logsMutex.withLock {
- loggingStore.updateLogs(logs)
+ Timber.d("sending update logs to store")
+ logsDataSource.updateLogs(logs)
}
}
}
while (true) {
+ Timber.d("in loop starting")
+
loggingRepository.startLogging(
terminal = loggingTerminal,
startingId = logs.lastOrNull()?.id ?: 0,
).catch { throwable ->
+ Timber.e(throwable)
+
if (throwable is TerminalNotSupportedException) {
if (appPreferences.fallbackToDefaultTerminal) {
toast(Strings.terminal_unavailable_falling_back)
@@ -150,7 +161,7 @@ class LoggingService : LifecycleService() {
} else {
delay(10000) // waiting for 10sec before new attempt
}
- } else {
+ } else if (throwable !is CancellationException) {
toast(getString(Strings.error, throwable.localizedMessage))
throwable.printStackTrace()
@@ -176,6 +187,7 @@ class LoggingService : LifecycleService() {
}
}
} finally {
+ Timber.d("finally block")
withContext(NonCancellable) {
recordingController.loggingStopped()
clearLogs().join()
@@ -187,16 +199,21 @@ class LoggingService : LifecycleService() {
}
private fun restartLogging() = lifecycleScope.launch {
+ Timber.d("restaring logs")
+
loggingJob?.cancelAndJoin()
+ Timber.d("cancelled loggingJob")
+
startLogging()
}
private fun clearLogs() = lifecycleScope.launch {
+ Timber.d("clearing logs")
logsMutex.withLock {
logs.clear()
}
- loggingStore.updateLogs(emptyList())
+ logsDataSource.updateLogs(emptyList())
}
private fun notification() = NotificationCompat.Builder(this, LOGGING_STATUS_CHANNEL_ID)
@@ -214,7 +231,10 @@ class LoggingService : LifecycleService() {
.build()
private fun killApp() = lifecycleScope.launch {
+ Timber.d("killing app")
+
loggingJob?.cancelAndJoin()
+ Timber.d("cancelled loggingJob and now can stop app")
activityManager.appTasks.forEach {
it.finishAndRemoveTask()
diff --git a/feature/logging/service/src/main/kotlin/com/f0x1d/logfox/feature/logging/service/presentation/LoggingServiceDelegateImpl.kt b/feature/logging/service/src/main/kotlin/com/f0x1d/logfox/feature/logging/service/presentation/LoggingServiceDelegateImpl.kt
new file mode 100644
index 00000000..85674f6c
--- /dev/null
+++ b/feature/logging/service/src/main/kotlin/com/f0x1d/logfox/feature/logging/service/presentation/LoggingServiceDelegateImpl.kt
@@ -0,0 +1,23 @@
+package com.f0x1d.logfox.feature.logging.service.presentation
+
+import android.content.Context
+import com.f0x1d.logfox.arch.sendService
+import com.f0x1d.logfox.feature.logging.api.presentation.LoggingServiceDelegate
+import dagger.hilt.android.qualifiers.ApplicationContext
+import javax.inject.Inject
+
+internal class LoggingServiceDelegateImpl @Inject constructor(
+ @ApplicationContext private val context: Context,
+) : LoggingServiceDelegate {
+ override fun clearLogs() {
+ context.sendService(LoggingService.ACTION_CLEAR_LOGS)
+ }
+
+ override fun restartLogging() {
+ context.sendService(LoggingService.ACTION_RESTART_LOGGING)
+ }
+
+ override fun killService() {
+ context.sendService(LoggingService.ACTION_KILL_SERVICE)
+ }
+}
diff --git a/feature/logging/src/main/kotlin/com/f0x1d/feature/logging/service/MainActivityPendingIntentProvider.kt b/feature/logging/service/src/main/kotlin/com/f0x1d/logfox/feature/logging/service/presentation/MainActivityPendingIntentProvider.kt
similarity index 66%
rename from feature/logging/src/main/kotlin/com/f0x1d/feature/logging/service/MainActivityPendingIntentProvider.kt
rename to feature/logging/service/src/main/kotlin/com/f0x1d/logfox/feature/logging/service/presentation/MainActivityPendingIntentProvider.kt
index ed9d04f6..705a014c 100644
--- a/feature/logging/src/main/kotlin/com/f0x1d/feature/logging/service/MainActivityPendingIntentProvider.kt
+++ b/feature/logging/service/src/main/kotlin/com/f0x1d/logfox/feature/logging/service/presentation/MainActivityPendingIntentProvider.kt
@@ -1,4 +1,4 @@
-package com.f0x1d.feature.logging.service
+package com.f0x1d.logfox.feature.logging.service.presentation
import android.app.PendingIntent
diff --git a/feature/logging/src/main/kotlin/com/f0x1d/feature/logging/viewmodel/LogsViewModel.kt b/feature/logging/src/main/kotlin/com/f0x1d/feature/logging/viewmodel/LogsViewModel.kt
deleted file mode 100644
index ee39a5ee..00000000
--- a/feature/logging/src/main/kotlin/com/f0x1d/feature/logging/viewmodel/LogsViewModel.kt
+++ /dev/null
@@ -1,178 +0,0 @@
-package com.f0x1d.feature.logging.viewmodel
-
-import android.app.Application
-import android.net.Uri
-import androidx.lifecycle.viewModelScope
-import com.f0x1d.feature.logging.di.FileUri
-import com.f0x1d.logfox.arch.di.DefaultDispatcher
-import com.f0x1d.logfox.arch.di.IODispatcher
-import com.f0x1d.logfox.arch.viewmodel.BaseViewModel
-import com.f0x1d.logfox.database.entity.UserFilter
-import com.f0x1d.logfox.datetime.DateTimeFormatter
-import com.f0x1d.logfox.feature.filters.core.repository.FiltersRepository
-import com.f0x1d.logfox.feature.logging.core.model.filterAndSearch
-import com.f0x1d.logfox.feature.logging.core.store.LoggingStore
-import com.f0x1d.logfox.feature.recordings.core.repository.RecordingsRepository
-import com.f0x1d.logfox.model.logline.LogLine
-import com.f0x1d.logfox.preferences.shared.AppPreferences
-import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.mapNotNull
-import kotlinx.coroutines.flow.scan
-import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.flow.update
-import kotlinx.coroutines.withContext
-import javax.inject.Inject
-
-@HiltViewModel
-class LogsViewModel @Inject constructor(
- @FileUri val fileUri: Uri?,
- private val loggingStore: LoggingStore,
- private val filtersRepository: FiltersRepository,
- private val recordingsRepository: RecordingsRepository,
- private val appPreferences: AppPreferences,
- @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher,
- @IODispatcher private val ioDispatcher: CoroutineDispatcher,
- dateTimeFormatter: DateTimeFormatter,
- application: Application,
-): BaseViewModel(application), DateTimeFormatter by dateTimeFormatter {
-
- val query = MutableStateFlow(null)
- val queryAndFilters = query.combine(
- filtersRepository.getAllEnabledAsFlow(),
- ) { query, filters -> query to filters }
-
- val paused = MutableStateFlow(false)
-
- val viewingFile = fileUri != null
- val viewingFileName = fileUri?.readFileName(ctx)
-
- val selectedItems = MutableStateFlow(emptySet())
-
- val selectedItemsContent get() = selectedItems.value.joinToString("\n") { line ->
- appPreferences.originalOf(
- logLine = line,
- formatDate = ::formatDate,
- formatTime = ::formatTime,
- )
- }
-
- val logs = combine(
- fileUri?.readFileContentsAsFlow(
- context = ctx,
- logsDisplayLimit = appPreferences.logsDisplayLimit,
- ) ?: loggingStore.logs,
- filtersRepository.getAllEnabledAsFlow(),
- query,
- if (!viewingFile) paused else flowOf(false)
- ) { logs, filters, query, paused ->
- LogsData(logs, filters, query, paused)
- }.scan(LogsData()) { accumulator, data ->
- when {
- !data.paused
- // In case they were cleared
- || data.logs.isEmpty() -> data
-
- data.query != accumulator.query
- || data.filters != accumulator.filters
- -> data.copy(
- logs = accumulator.logs,
- )
-
- else -> data.copy(
- logs = accumulator.logs,
- passing = false,
- )
- }
- }.filter { data ->
- data.passing
- }.mapNotNull { data ->
- data.logs.filterAndSearch(
- filters = data.filters,
- query = data.query,
- )
- }.flowOn(
- defaultDispatcher,
- ).stateIn(
- scope = viewModelScope,
- started = SharingStarted.Eagerly,
- initialValue = emptyList(),
- )
-
- val resumeLoggingWithBottomTouch get() = appPreferences.resumeLoggingWithBottomTouch
- val logsTextSize get() = appPreferences.logsTextSize.toFloat()
- val logsExpanded get() = appPreferences.logsExpanded
- val logsFormat get() = appPreferences.showLogValues
-
- fun selectLine(logLine: LogLine, selected: Boolean) = selectedItems.updateSet {
- if (selected) add(
- logLine
- ) else remove(
- logLine
- )
- }
-
- fun selectAll() {
- if (selectedItems.value.containsAll(logs.value)) selectedItems.update {
- emptySet()
- } else selectedItems.update {
- logs.value.toSet()
- }
- }
-
- fun selectedToRecording() = launchCatching {
- recordingsRepository.createRecordingFrom(
- lines = withContext(defaultDispatcher) {
- selectedItems.value.sortedBy { it.dateAndTime }
- },
- )
- }
-
- fun exportSelectedLogsTo(uri: Uri) = launchCatching(ioDispatcher) {
- ctx.contentResolver.openOutputStream(uri)?.use {
- it.write(
- selectedItems.value.joinToString("\n") { line ->
- appPreferences.originalOf(
- logLine = line,
- formatDate = ::formatDate,
- formatTime = ::formatTime,
- )
- }.encodeToByteArray()
- )
- }
- }
-
- fun query(query: String?) = this.query.update { query }
-
- fun switchState() = if (!paused.value)
- pause()
- else
- resume()
-
- fun pause() = paused.update { true }
- fun resume() = paused.update { false }
-
- fun originalOf(logLine: LogLine): String = appPreferences.originalOf(
- logLine = logLine,
- formatDate = ::formatDate,
- formatTime = ::formatTime,
- )
-
- private fun MutableStateFlow>.updateSet(block: MutableSet.() -> Unit) = update {
- it.toMutableSet().apply(block).toSet()
- }
-
- private data class LogsData(
- val logs: List = emptyList(),
- val filters: List = emptyList(),
- val query: String? = null,
- val paused: Boolean = false,
- val passing: Boolean = true,
- )
-}
diff --git a/feature/recordings-core/build.gradle.kts b/feature/recordings-core/build.gradle.kts
deleted file mode 100644
index 3812f347..00000000
--- a/feature/recordings-core/build.gradle.kts
+++ /dev/null
@@ -1,9 +0,0 @@
-plugins {
- id("logfox.android.feature")
-}
-
-android.namespace = "com.f0x1d.logfox.feature.recordings.core"
-
-dependencies {
- implementation(projects.feature.loggingCore)
-}
diff --git a/feature/recordings/api/.gitignore b/feature/recordings/api/.gitignore
new file mode 100644
index 00000000..567609b1
--- /dev/null
+++ b/feature/recordings/api/.gitignore
@@ -0,0 +1 @@
+build/
diff --git a/feature/recordings/api/build.gradle.kts b/feature/recordings/api/build.gradle.kts
new file mode 100644
index 00000000..69801a3a
--- /dev/null
+++ b/feature/recordings/api/build.gradle.kts
@@ -0,0 +1,9 @@
+plugins {
+ id("logfox.android.feature")
+}
+
+android.namespace = "com.f0x1d.logfox.feature.recordings.api"
+
+dependencies {
+
+}
diff --git a/feature/recordings/api/src/main/kotlin/com/f0x1d/logfox/feature/recordings/api/data/RecordingController.kt b/feature/recordings/api/src/main/kotlin/com/f0x1d/logfox/feature/recordings/api/data/RecordingController.kt
new file mode 100644
index 00000000..072e2f56
--- /dev/null
+++ b/feature/recordings/api/src/main/kotlin/com/f0x1d/logfox/feature/recordings/api/data/RecordingController.kt
@@ -0,0 +1,21 @@
+package com.f0x1d.logfox.feature.recordings.api.data
+
+import com.f0x1d.logfox.database.entity.LogRecording
+import com.f0x1d.logfox.model.logline.LogLine
+import kotlinx.coroutines.flow.StateFlow
+
+interface RecordingController {
+ val recordingState: StateFlow
+ val reader: suspend (LogLine) -> Unit
+
+ suspend fun record()
+ suspend fun pause()
+ suspend fun resume()
+ suspend fun end(): LogRecording?
+
+ suspend fun loggingStopped()
+}
+
+enum class RecordingState {
+ IDLE, RECORDING, PAUSED, SAVING
+}
diff --git a/feature/recordings/api/src/main/kotlin/com/f0x1d/logfox/feature/recordings/api/data/RecordingNotificationController.kt b/feature/recordings/api/src/main/kotlin/com/f0x1d/logfox/feature/recordings/api/data/RecordingNotificationController.kt
new file mode 100644
index 00000000..52d00b1b
--- /dev/null
+++ b/feature/recordings/api/src/main/kotlin/com/f0x1d/logfox/feature/recordings/api/data/RecordingNotificationController.kt
@@ -0,0 +1,13 @@
+package com.f0x1d.logfox.feature.recordings.api.data
+
+interface RecordingNotificationController {
+
+ companion object {
+ const val RECORDING_NOTIFICATIONS_TAG = "recording"
+ const val RECORDING_NOTIFICATIONS_ID = 0
+ }
+
+ fun sendRecordingNotification()
+ fun sendRecordingPausedNotification()
+ fun cancelRecordingNotification()
+}
diff --git a/feature/recordings/api/src/main/kotlin/com/f0x1d/logfox/feature/recordings/api/data/RecordingsRepository.kt b/feature/recordings/api/src/main/kotlin/com/f0x1d/logfox/feature/recordings/api/data/RecordingsRepository.kt
new file mode 100644
index 00000000..7d83104d
--- /dev/null
+++ b/feature/recordings/api/src/main/kotlin/com/f0x1d/logfox/feature/recordings/api/data/RecordingsRepository.kt
@@ -0,0 +1,12 @@
+package com.f0x1d.logfox.feature.recordings.api.data
+
+import com.f0x1d.logfox.arch.repository.DatabaseProxyRepository
+import com.f0x1d.logfox.database.entity.LogRecording
+import com.f0x1d.logfox.model.logline.LogLine
+
+interface RecordingsRepository : DatabaseProxyRepository {
+ suspend fun saveAll(): LogRecording
+ suspend fun createRecordingFrom(lines: List)
+
+ suspend fun updateTitle(logRecording: LogRecording, newTitle: String)
+}
diff --git a/feature/recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/controller/reader/RecordingReader.kt b/feature/recordings/api/src/main/kotlin/com/f0x1d/logfox/feature/recordings/api/data/reader/RecordingReader.kt
similarity index 97%
rename from feature/recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/controller/reader/RecordingReader.kt
rename to feature/recordings/api/src/main/kotlin/com/f0x1d/logfox/feature/recordings/api/data/reader/RecordingReader.kt
index 02eb4e4c..e2cc655f 100644
--- a/feature/recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/controller/reader/RecordingReader.kt
+++ b/feature/recordings/api/src/main/kotlin/com/f0x1d/logfox/feature/recordings/api/data/reader/RecordingReader.kt
@@ -1,4 +1,4 @@
-package com.f0x1d.logfox.feature.recordings.core.controller.reader
+package com.f0x1d.logfox.feature.recordings.api.data.reader
import com.f0x1d.logfox.datetime.DateTimeFormatter
import com.f0x1d.logfox.model.logline.LogLine
diff --git a/feature/recordings/build.gradle.kts b/feature/recordings/build.gradle.kts
deleted file mode 100644
index 307d674f..00000000
--- a/feature/recordings/build.gradle.kts
+++ /dev/null
@@ -1,9 +0,0 @@
-plugins {
- id("logfox.android.feature")
-}
-
-android.namespace = "com.f0x1d.logfox.feature.recordings"
-
-dependencies {
- implementation(projects.feature.recordingsCore)
-}
diff --git a/feature/recordings/details/.gitignore b/feature/recordings/details/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/feature/recordings/details/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/feature/recordings/details/build.gradle.kts b/feature/recordings/details/build.gradle.kts
new file mode 100644
index 00000000..ede4b50e
--- /dev/null
+++ b/feature/recordings/details/build.gradle.kts
@@ -0,0 +1,10 @@
+plugins {
+ id("logfox.android.feature")
+}
+
+android.namespace = "com.f0x1d.logfox.feature.recordings.details"
+
+dependencies {
+ implementation(projects.feature.recordings.api)
+ implementation(projects.feature.logging.api)
+}
diff --git a/feature/recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/di/RecordingViewModelModule.kt b/feature/recordings/details/src/main/kotlin/com/f0x1d/logfox/feature/recordings/details/di/RecordingDetailsViewModelModule.kt
similarity index 91%
rename from feature/recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/di/RecordingViewModelModule.kt
rename to feature/recordings/details/src/main/kotlin/com/f0x1d/logfox/feature/recordings/details/di/RecordingDetailsViewModelModule.kt
index e37cfe0e..a4c80b0a 100644
--- a/feature/recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/di/RecordingViewModelModule.kt
+++ b/feature/recordings/details/src/main/kotlin/com/f0x1d/logfox/feature/recordings/details/di/RecordingDetailsViewModelModule.kt
@@ -1,4 +1,4 @@
-package com.f0x1d.logfox.feature.recordings.di
+package com.f0x1d.logfox.feature.recordings.details.di
import androidx.lifecycle.SavedStateHandle
import dagger.Module
diff --git a/feature/recordings/details/src/main/kotlin/com/f0x1d/logfox/feature/recordings/details/presentation/RecordingDetailsAction.kt b/feature/recordings/details/src/main/kotlin/com/f0x1d/logfox/feature/recordings/details/presentation/RecordingDetailsAction.kt
new file mode 100644
index 00000000..6fd3ba86
--- /dev/null
+++ b/feature/recordings/details/src/main/kotlin/com/f0x1d/logfox/feature/recordings/details/presentation/RecordingDetailsAction.kt
@@ -0,0 +1,3 @@
+package com.f0x1d.logfox.feature.recordings.details.presentation
+
+sealed interface RecordingDetailsAction
diff --git a/feature/recordings/details/src/main/kotlin/com/f0x1d/logfox/feature/recordings/details/presentation/RecordingDetailsState.kt b/feature/recordings/details/src/main/kotlin/com/f0x1d/logfox/feature/recordings/details/presentation/RecordingDetailsState.kt
new file mode 100644
index 00000000..c4a9209f
--- /dev/null
+++ b/feature/recordings/details/src/main/kotlin/com/f0x1d/logfox/feature/recordings/details/presentation/RecordingDetailsState.kt
@@ -0,0 +1,7 @@
+package com.f0x1d.logfox.feature.recordings.details.presentation
+
+import com.f0x1d.logfox.database.entity.LogRecording
+
+data class RecordingDetailsState(
+ val recording: LogRecording? = null,
+)
diff --git a/feature/recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/viewmodel/RecordingViewModel.kt b/feature/recordings/details/src/main/kotlin/com/f0x1d/logfox/feature/recordings/details/presentation/RecordingDetailsViewModel.kt
similarity index 63%
rename from feature/recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/viewmodel/RecordingViewModel.kt
rename to feature/recordings/details/src/main/kotlin/com/f0x1d/logfox/feature/recordings/details/presentation/RecordingDetailsViewModel.kt
index a7ad50c1..fa7713e3 100644
--- a/feature/recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/viewmodel/RecordingViewModel.kt
+++ b/feature/recordings/details/src/main/kotlin/com/f0x1d/logfox/feature/recordings/details/presentation/RecordingDetailsViewModel.kt
@@ -1,4 +1,4 @@
-package com.f0x1d.logfox.feature.recordings.viewmodel
+package com.f0x1d.logfox.feature.recordings.details.presentation
import android.app.Application
import android.net.Uri
@@ -8,49 +8,53 @@ import com.f0x1d.logfox.arch.io.exportToZip
import com.f0x1d.logfox.arch.io.putZipEntry
import com.f0x1d.logfox.arch.viewmodel.BaseViewModel
import com.f0x1d.logfox.datetime.DateTimeFormatter
-import com.f0x1d.logfox.feature.recordings.core.repository.RecordingsRepository
-import com.f0x1d.logfox.feature.recordings.di.RecordingId
+import com.f0x1d.logfox.feature.recordings.api.data.RecordingsRepository
+import com.f0x1d.logfox.feature.recordings.details.di.RecordingId
import com.f0x1d.logfox.model.deviceData
import com.f0x1d.logfox.preferences.shared.AppPreferences
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.flow.take
+import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import javax.inject.Inject
@HiltViewModel
-class RecordingViewModel @Inject constructor(
+class RecordingDetailsViewModel @Inject constructor(
@RecordingId val recordingId: Long,
private val recordingsRepository: RecordingsRepository,
private val appPreferences: AppPreferences,
@IODispatcher private val ioDispatcher: CoroutineDispatcher,
dateTimeFormatter: DateTimeFormatter,
application: Application
-): BaseViewModel(application), DateTimeFormatter by dateTimeFormatter {
+): BaseViewModel(
+ initialStateProvider = { RecordingDetailsState() },
+ application = application,
+), DateTimeFormatter by dateTimeFormatter {
+ var currentTitle: String? = null
- val recording = recordingsRepository.getByIdAsFlow(recordingId)
- .distinctUntilChanged()
- .onEach { recording ->
- currentTitle.update { recording?.title }
- }
- .stateIn(
- scope = viewModelScope,
- started = SharingStarted.Eagerly,
- initialValue = null,
- )
+ private val titleUpdateMutex = Mutex()
- val currentTitle = MutableStateFlow(null)
+ init {
+ load()
+ }
- private val titleUpdateMutex = Mutex()
+ private fun load() {
+ viewModelScope.launch {
+ recordingsRepository.getByIdAsFlow(recordingId)
+ .distinctUntilChanged()
+ .take(1)
+ .collect { recording ->
+ currentTitle = recording?.title
+ reduce { copy(recording = recording) }
+ }
+ }
+ }
fun exportFile(uri: Uri) = launchCatching(ioDispatcher) {
- val recording = recording.value ?: return@launchCatching
+ val recording = currentState.recording ?: return@launchCatching
ctx.contentResolver.openOutputStream(uri)?.use { outputStream ->
recording.file.inputStream().use { inputStream ->
@@ -60,7 +64,7 @@ class RecordingViewModel @Inject constructor(
}
fun exportZipFile(uri: Uri) = launchCatching(ioDispatcher) {
- val recording = recording.value ?: return@launchCatching
+ val recording = currentState.recording ?: return@launchCatching
ctx.contentResolver.openOutputStream(uri)?.use {
it.exportToZip {
@@ -79,7 +83,7 @@ class RecordingViewModel @Inject constructor(
fun updateTitle(title: String) = launchCatching {
titleUpdateMutex.withLock {
- recording.value?.let {
+ currentState.recording?.let {
recordingsRepository.updateTitle(it, title)
}
}
diff --git a/feature/recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/ui/dialog/RecordingBottomSheet.kt b/feature/recordings/details/src/main/kotlin/com/f0x1d/logfox/feature/recordings/details/presentation/ui/RecordingDetailsBottomSheetFragment.kt
similarity index 56%
rename from feature/recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/ui/dialog/RecordingBottomSheet.kt
rename to feature/recordings/details/src/main/kotlin/com/f0x1d/logfox/feature/recordings/details/presentation/ui/RecordingDetailsBottomSheetFragment.kt
index 6528933f..417dc4c7 100644
--- a/feature/recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/ui/dialog/RecordingBottomSheet.kt
+++ b/feature/recordings/details/src/main/kotlin/com/f0x1d/logfox/feature/recordings/details/presentation/ui/RecordingDetailsBottomSheetFragment.kt
@@ -1,4 +1,4 @@
-package com.f0x1d.logfox.feature.recordings.ui.dialog
+package com.f0x1d.logfox.feature.recordings.details.presentation.ui
import android.os.Bundle
import android.view.LayoutInflater
@@ -10,20 +10,18 @@ import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import com.f0x1d.logfox.arch.asUri
+import com.f0x1d.logfox.arch.presentation.ui.dialog.BaseBottomSheetFragment
import com.f0x1d.logfox.arch.shareFileIntent
-import com.f0x1d.logfox.arch.ui.dialog.BaseViewModelBottomSheet
-import com.f0x1d.logfox.feature.recordings.databinding.SheetRecordingBinding
-import com.f0x1d.logfox.feature.recordings.viewmodel.RecordingViewModel
+import com.f0x1d.logfox.feature.recordings.details.databinding.SheetRecordingDetailsBinding
+import com.f0x1d.logfox.feature.recordings.details.presentation.RecordingDetailsViewModel
import com.f0x1d.logfox.navigation.Directions
import dagger.hilt.android.AndroidEntryPoint
-import kotlinx.coroutines.flow.filterNotNull
-import kotlinx.coroutines.flow.take
import java.util.Date
@AndroidEntryPoint
-class RecordingBottomSheet: BaseViewModelBottomSheet() {
+class RecordingDetailsBottomSheetFragment: BaseBottomSheetFragment() {
- override val viewModel by viewModels()
+ private val viewModel by viewModels()
private val zipLogLauncher = registerForActivityResult(
ActivityResultContracts.CreateDocument("application/zip")
@@ -40,15 +38,11 @@ class RecordingBottomSheet: BaseViewModelBottomSheet
- if (logRecording == null) return@collectWithLifecycle
-
- timeText.text = Date(logRecording.dateAndTime).toLocaleString()
-
- viewButton.setOnClickListener {
+ override fun SheetRecordingDetailsBinding.onViewCreated(view: View, savedInstanceState: Bundle?) {
+ viewButton.setOnClickListener {
+ viewModel.currentState.recording?.let { logRecording ->
findNavController().navigate(
resId = Directions.action_global_logsFragment_from_recordingBottomSheet,
args = bundleOf(
@@ -56,22 +50,31 @@ class RecordingBottomSheet: BaseViewModelBottomSheet
logExportLauncher.launch("${viewModel.formatForExport(logRecording.dateAndTime)}.log")
}
- shareButton.setOnClickListener {
+ }
+ shareButton.setOnClickListener {
+ viewModel.currentState.recording?.let { logRecording ->
requireContext().shareFileIntent(logRecording.file)
}
- zipButton.setOnClickListener {
+ }
+ zipButton.setOnClickListener {
+ viewModel.currentState.recording?.let { logRecording ->
zipLogLauncher.launch("${viewModel.formatForExport(logRecording.dateAndTime)}.zip")
}
}
- viewModel.currentTitle.filterNotNull().take(1).collectWithLifecycle {
- title.apply {
- setText(it)
- doAfterTextChanged { viewModel.updateTitle(it?.toString() ?: "") }
- }
+ title.doAfterTextChanged { viewModel.updateTitle(it?.toString().orEmpty()) }
+
+ viewModel.state.collectWithLifecycle { state ->
+ title.setText(viewModel.currentTitle.orEmpty())
+
+ val logRecording = state.recording ?: return@collectWithLifecycle
+
+ timeText.text = Date(logRecording.dateAndTime).toLocaleString()
}
}
}
diff --git a/feature/recordings/src/main/res/layout/sheet_recording.xml b/feature/recordings/details/src/main/res/layout/sheet_recording_details.xml
similarity index 100%
rename from feature/recordings/src/main/res/layout/sheet_recording.xml
rename to feature/recordings/details/src/main/res/layout/sheet_recording_details.xml
diff --git a/feature/recordings/impl/.gitignore b/feature/recordings/impl/.gitignore
new file mode 100644
index 00000000..567609b1
--- /dev/null
+++ b/feature/recordings/impl/.gitignore
@@ -0,0 +1 @@
+build/
diff --git a/feature/recordings/impl/build.gradle.kts b/feature/recordings/impl/build.gradle.kts
new file mode 100644
index 00000000..d322f123
--- /dev/null
+++ b/feature/recordings/impl/build.gradle.kts
@@ -0,0 +1,10 @@
+plugins {
+ id("logfox.android.feature")
+}
+
+android.namespace = "com.f0x1d.logfox.feature.recordings.impl"
+
+dependencies {
+ implementation(projects.feature.recordings.api)
+ implementation(projects.feature.logging.api)
+}
diff --git a/feature/recordings-core/src/main/AndroidManifest.xml b/feature/recordings/impl/src/main/AndroidManifest.xml
similarity index 67%
rename from feature/recordings-core/src/main/AndroidManifest.xml
rename to feature/recordings/impl/src/main/AndroidManifest.xml
index 39a15abf..4f7220e0 100644
--- a/feature/recordings-core/src/main/AndroidManifest.xml
+++ b/feature/recordings/impl/src/main/AndroidManifest.xml
@@ -2,7 +2,7 @@
-
+
diff --git a/feature/recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/controller/RecordingController.kt b/feature/recordings/impl/src/main/kotlin/com/f0x1d/logfox/feature/recordings/impl/data/RecordingControllerImpl.kt
similarity index 85%
rename from feature/recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/controller/RecordingController.kt
rename to feature/recordings/impl/src/main/kotlin/com/f0x1d/logfox/feature/recordings/impl/data/RecordingControllerImpl.kt
index e1f57ba3..0bb28565 100644
--- a/feature/recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/controller/RecordingController.kt
+++ b/feature/recordings/impl/src/main/kotlin/com/f0x1d/logfox/feature/recordings/impl/data/RecordingControllerImpl.kt
@@ -1,12 +1,14 @@
-package com.f0x1d.logfox.feature.recordings.core.controller
+package com.f0x1d.logfox.feature.recordings.impl.data
import android.content.Context
import com.f0x1d.logfox.arch.di.IODispatcher
import com.f0x1d.logfox.database.AppDatabase
import com.f0x1d.logfox.database.entity.LogRecording
import com.f0x1d.logfox.datetime.DateTimeFormatter
-import com.f0x1d.logfox.feature.recordings.core.controller.reader.RecordingReader
-import com.f0x1d.logfox.model.logline.LogLine
+import com.f0x1d.logfox.feature.recordings.api.data.RecordingController
+import com.f0x1d.logfox.feature.recordings.api.data.RecordingNotificationController
+import com.f0x1d.logfox.feature.recordings.api.data.RecordingState
+import com.f0x1d.logfox.feature.recordings.api.data.reader.RecordingReader
import com.f0x1d.logfox.strings.Strings
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineDispatcher
@@ -18,22 +20,6 @@ import java.io.File
import javax.inject.Inject
import javax.inject.Singleton
-interface RecordingController {
- val recordingState: StateFlow
- val reader: suspend (LogLine) -> Unit
-
- suspend fun record()
- suspend fun pause()
- suspend fun resume()
- suspend fun end(): LogRecording?
-
- suspend fun loggingStopped()
-}
-
-enum class RecordingState {
- IDLE, RECORDING, PAUSED, SAVING
-}
-
@Singleton
internal class RecordingControllerImpl @Inject constructor(
@ApplicationContext private val context: Context,
@@ -106,3 +92,4 @@ internal class RecordingControllerImpl @Inject constructor(
}
}
}
+
diff --git a/feature/recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/controller/RecordingNotificationController.kt b/feature/recordings/impl/src/main/kotlin/com/f0x1d/logfox/feature/recordings/impl/data/RecordingNotificationControllerImpl.kt
similarity index 89%
rename from feature/recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/controller/RecordingNotificationController.kt
rename to feature/recordings/impl/src/main/kotlin/com/f0x1d/logfox/feature/recordings/impl/data/RecordingNotificationControllerImpl.kt
index 349a2a2a..aaded57b 100644
--- a/feature/recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/controller/RecordingNotificationController.kt
+++ b/feature/recordings/impl/src/main/kotlin/com/f0x1d/logfox/feature/recordings/impl/data/RecordingNotificationControllerImpl.kt
@@ -1,4 +1,4 @@
-package com.f0x1d.logfox.feature.recordings.core.controller
+package com.f0x1d.logfox.feature.recordings.impl.data
import android.annotation.SuppressLint
import android.content.Context
@@ -10,24 +10,13 @@ import com.f0x1d.logfox.arch.STOP_RECORDING_INTENT_ID
import com.f0x1d.logfox.arch.doIfNotificationsAllowed
import com.f0x1d.logfox.arch.makeBroadcastPendingIntent
import com.f0x1d.logfox.arch.notificationManagerCompat
-import com.f0x1d.logfox.feature.recordings.core.receiver.RecordingReceiver
+import com.f0x1d.logfox.feature.recordings.api.data.RecordingNotificationController
+import com.f0x1d.logfox.feature.recordings.impl.presentation.receiver.RecordingReceiver
import com.f0x1d.logfox.strings.Strings
import com.f0x1d.logfox.ui.Icons
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
-interface RecordingNotificationController {
-
- companion object {
- internal const val RECORDING_NOTIFICATIONS_TAG = "recording"
- internal const val RECORDING_NOTIFICATIONS_ID = 0
- }
-
- fun sendRecordingNotification()
- fun sendRecordingPausedNotification()
- fun cancelRecordingNotification()
-}
-
@SuppressLint("MissingPermission")
internal class RecordingNotificationControllerImpl @Inject constructor(
@ApplicationContext private val context: Context,
diff --git a/feature/recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/repository/RecordingsRepository.kt b/feature/recordings/impl/src/main/kotlin/com/f0x1d/logfox/feature/recordings/impl/data/RecordingsRepositoryImpl.kt
similarity index 91%
rename from feature/recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/repository/RecordingsRepository.kt
rename to feature/recordings/impl/src/main/kotlin/com/f0x1d/logfox/feature/recordings/impl/data/RecordingsRepositoryImpl.kt
index 9f1a24b3..a5bce391 100644
--- a/feature/recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/repository/RecordingsRepository.kt
+++ b/feature/recordings/impl/src/main/kotlin/com/f0x1d/logfox/feature/recordings/impl/data/RecordingsRepositoryImpl.kt
@@ -1,14 +1,14 @@
-package com.f0x1d.logfox.feature.recordings.core.repository
+package com.f0x1d.logfox.feature.recordings.impl.data
import android.content.Context
import com.f0x1d.logfox.arch.di.IODispatcher
import com.f0x1d.logfox.arch.di.MainDispatcher
-import com.f0x1d.logfox.arch.repository.DatabaseProxyRepository
import com.f0x1d.logfox.arch.toast
import com.f0x1d.logfox.database.AppDatabase
import com.f0x1d.logfox.database.entity.LogRecording
import com.f0x1d.logfox.datetime.DateTimeFormatter
-import com.f0x1d.logfox.feature.logging.core.repository.LoggingRepository
+import com.f0x1d.logfox.feature.logging.api.data.LoggingRepository
+import com.f0x1d.logfox.feature.recordings.api.data.RecordingsRepository
import com.f0x1d.logfox.model.logline.LogLine
import com.f0x1d.logfox.preferences.shared.AppPreferences
import com.f0x1d.logfox.strings.Strings
@@ -24,13 +24,6 @@ import java.io.FileOutputStream
import java.io.IOException
import javax.inject.Inject
-interface RecordingsRepository : DatabaseProxyRepository {
- suspend fun saveAll(): LogRecording
- suspend fun createRecordingFrom(lines: List)
-
- suspend fun updateTitle(logRecording: LogRecording, newTitle: String)
-}
-
internal class RecordingsRepositoryImpl @Inject constructor(
@ApplicationContext private val context: Context,
private val database: AppDatabase,
diff --git a/feature/recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/di/ControllersModule.kt b/feature/recordings/impl/src/main/kotlin/com/f0x1d/logfox/feature/recordings/impl/di/ControllersModule.kt
similarity index 56%
rename from feature/recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/di/ControllersModule.kt
rename to feature/recordings/impl/src/main/kotlin/com/f0x1d/logfox/feature/recordings/impl/di/ControllersModule.kt
index 1ddd982d..0e16b60d 100644
--- a/feature/recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/di/ControllersModule.kt
+++ b/feature/recordings/impl/src/main/kotlin/com/f0x1d/logfox/feature/recordings/impl/di/ControllersModule.kt
@@ -1,9 +1,9 @@
-package com.f0x1d.logfox.feature.recordings.core.di
+package com.f0x1d.logfox.feature.recordings.impl.di
-import com.f0x1d.logfox.feature.recordings.core.controller.RecordingController
-import com.f0x1d.logfox.feature.recordings.core.controller.RecordingControllerImpl
-import com.f0x1d.logfox.feature.recordings.core.controller.RecordingNotificationController
-import com.f0x1d.logfox.feature.recordings.core.controller.RecordingNotificationControllerImpl
+import com.f0x1d.logfox.feature.recordings.api.data.RecordingController
+import com.f0x1d.logfox.feature.recordings.api.data.RecordingNotificationController
+import com.f0x1d.logfox.feature.recordings.impl.data.RecordingControllerImpl
+import com.f0x1d.logfox.feature.recordings.impl.data.RecordingNotificationControllerImpl
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
diff --git a/feature/recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/di/RepositoriesModule.kt b/feature/recordings/impl/src/main/kotlin/com/f0x1d/logfox/feature/recordings/impl/di/RepositoriesModule.kt
similarity index 61%
rename from feature/recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/di/RepositoriesModule.kt
rename to feature/recordings/impl/src/main/kotlin/com/f0x1d/logfox/feature/recordings/impl/di/RepositoriesModule.kt
index 21658f0e..2caeca14 100644
--- a/feature/recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/di/RepositoriesModule.kt
+++ b/feature/recordings/impl/src/main/kotlin/com/f0x1d/logfox/feature/recordings/impl/di/RepositoriesModule.kt
@@ -1,7 +1,7 @@
-package com.f0x1d.logfox.feature.recordings.core.di
+package com.f0x1d.logfox.feature.recordings.impl.di
-import com.f0x1d.logfox.feature.recordings.core.repository.RecordingsRepository
-import com.f0x1d.logfox.feature.recordings.core.repository.RecordingsRepositoryImpl
+import com.f0x1d.logfox.feature.recordings.api.data.RecordingsRepository
+import com.f0x1d.logfox.feature.recordings.impl.data.RecordingsRepositoryImpl
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
diff --git a/feature/recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/receiver/RecordingReceiver.kt b/feature/recordings/impl/src/main/kotlin/com/f0x1d/logfox/feature/recordings/impl/presentation/receiver/RecordingReceiver.kt
similarity index 89%
rename from feature/recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/receiver/RecordingReceiver.kt
rename to feature/recordings/impl/src/main/kotlin/com/f0x1d/logfox/feature/recordings/impl/presentation/receiver/RecordingReceiver.kt
index c585a0e9..1876e288 100644
--- a/feature/recordings-core/src/main/kotlin/com/f0x1d/logfox/feature/recordings/core/receiver/RecordingReceiver.kt
+++ b/feature/recordings/impl/src/main/kotlin/com/f0x1d/logfox/feature/recordings/impl/presentation/receiver/RecordingReceiver.kt
@@ -1,10 +1,10 @@
-package com.f0x1d.logfox.feature.recordings.core.receiver
+package com.f0x1d.logfox.feature.recordings.impl.presentation.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.f0x1d.logfox.arch.di.MainDispatcher
-import com.f0x1d.logfox.feature.recordings.core.controller.RecordingController
+import com.f0x1d.logfox.feature.recordings.api.data.RecordingController
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
diff --git a/feature/recordings/list/.gitignore b/feature/recordings/list/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/feature/recordings/list/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/feature/recordings/list/build.gradle.kts b/feature/recordings/list/build.gradle.kts
new file mode 100644
index 00000000..df914d1a
--- /dev/null
+++ b/feature/recordings/list/build.gradle.kts
@@ -0,0 +1,10 @@
+plugins {
+ id("logfox.android.feature")
+}
+
+android.namespace = "com.f0x1d.logfox.feature.recordings.list"
+
+dependencies {
+ implementation(projects.feature.recordings.api)
+ implementation(projects.feature.logging.api)
+}
diff --git a/feature/recordings/list/src/main/kotlin/com/f0x1d/logfox/feature/recordings/list/presentation/RecordingsAction.kt b/feature/recordings/list/src/main/kotlin/com/f0x1d/logfox/feature/recordings/list/presentation/RecordingsAction.kt
new file mode 100644
index 00000000..30807173
--- /dev/null
+++ b/feature/recordings/list/src/main/kotlin/com/f0x1d/logfox/feature/recordings/list/presentation/RecordingsAction.kt
@@ -0,0 +1,8 @@
+package com.f0x1d.logfox.feature.recordings.list.presentation
+
+import com.f0x1d.logfox.database.entity.LogRecording
+
+sealed interface RecordingsAction {
+ data class ShowSnackbar(val text: String) : RecordingsAction
+ data class OpenRecording(val recording: LogRecording) : RecordingsAction
+}
diff --git a/feature/recordings/list/src/main/kotlin/com/f0x1d/logfox/feature/recordings/list/presentation/RecordingsState.kt b/feature/recordings/list/src/main/kotlin/com/f0x1d/logfox/feature/recordings/list/presentation/RecordingsState.kt
new file mode 100644
index 00000000..b097019f
--- /dev/null
+++ b/feature/recordings/list/src/main/kotlin/com/f0x1d/logfox/feature/recordings/list/presentation/RecordingsState.kt
@@ -0,0 +1,9 @@
+package com.f0x1d.logfox.feature.recordings.list.presentation
+
+import com.f0x1d.logfox.database.entity.LogRecording
+import com.f0x1d.logfox.feature.recordings.api.data.RecordingState
+
+data class RecordingsState(
+ val recordings: List = emptyList(),
+ val recordingState: RecordingState = RecordingState.IDLE,
+)
diff --git a/feature/recordings/list/src/main/kotlin/com/f0x1d/logfox/feature/recordings/list/presentation/RecordingsViewModel.kt b/feature/recordings/list/src/main/kotlin/com/f0x1d/logfox/feature/recordings/list/presentation/RecordingsViewModel.kt
new file mode 100644
index 00000000..6999803a
--- /dev/null
+++ b/feature/recordings/list/src/main/kotlin/com/f0x1d/logfox/feature/recordings/list/presentation/RecordingsViewModel.kt
@@ -0,0 +1,78 @@
+package com.f0x1d.logfox.feature.recordings.list.presentation
+
+import android.app.Application
+import androidx.lifecycle.viewModelScope
+import com.f0x1d.logfox.arch.viewmodel.BaseViewModel
+import com.f0x1d.logfox.database.entity.LogRecording
+import com.f0x1d.logfox.feature.recordings.api.data.RecordingController
+import com.f0x1d.logfox.feature.recordings.api.data.RecordingState
+import com.f0x1d.logfox.feature.recordings.api.data.RecordingsRepository
+import com.f0x1d.logfox.strings.Strings
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class RecordingsViewModel @Inject constructor(
+ private val recordingsRepository: RecordingsRepository,
+ private val recordingController: RecordingController,
+ application: Application,
+): BaseViewModel(
+ initialStateProvider = { RecordingsState() },
+ application = application,
+) {
+ private val recordingState = recordingController.recordingState
+
+ init {
+ load()
+ }
+
+ private fun load() {
+ viewModelScope.launch {
+ combine(
+ recordingsRepository.getAllAsFlow().distinctUntilChanged(),
+ recordingState,
+ ) { recordings, recordingState ->
+ RecordingsState(
+ recordings = recordings,
+ recordingState = recordingState,
+ )
+ }.collect {
+ reduce { it }
+ }
+ }
+ }
+
+ fun toggleStartStop() = launchCatching {
+ if (recordingState.value == RecordingState.IDLE)
+ recordingController.record()
+ else
+ recordingController.end()?.also {
+ sendAction(RecordingsAction.OpenRecording(it))
+ }
+ }
+
+ fun togglePauseResume() = launchCatching {
+ if (recordingState.value == RecordingState.PAUSED)
+ recordingController.resume()
+ else
+ recordingController.pause()
+ }
+
+ fun clearRecordings() = launchCatching {
+ recordingsRepository.clear()
+ }
+
+ fun saveAll() = launchCatching {
+ sendAction(RecordingsAction.ShowSnackbar(ctx.getString(Strings.saving_logs)))
+ recordingsRepository.saveAll().also {
+ sendAction(RecordingsAction.OpenRecording(it))
+ }
+ }
+
+ fun delete(logRecording: LogRecording) = launchCatching {
+ recordingsRepository.delete(logRecording)
+ }
+}
diff --git a/feature/recordings/list/src/main/kotlin/com/f0x1d/logfox/feature/recordings/list/presentation/adapter/RecordingsAdapter.kt b/feature/recordings/list/src/main/kotlin/com/f0x1d/logfox/feature/recordings/list/presentation/adapter/RecordingsAdapter.kt
new file mode 100644
index 00000000..e6ce5c6f
--- /dev/null
+++ b/feature/recordings/list/src/main/kotlin/com/f0x1d/logfox/feature/recordings/list/presentation/adapter/RecordingsAdapter.kt
@@ -0,0 +1,22 @@
+package com.f0x1d.logfox.feature.recordings.list.presentation.adapter
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import com.f0x1d.logfox.arch.presentation.adapter.BaseListAdapter
+import com.f0x1d.logfox.database.entity.LogRecording
+import com.f0x1d.logfox.feature.recordings.list.databinding.ItemRecordingBinding
+import com.f0x1d.logfox.feature.recordings.list.presentation.ui.viewholder.RecordingViewHolder
+import com.f0x1d.logfox.model.diffCallback
+
+class RecordingsAdapter(
+ private val click: (LogRecording) -> Unit,
+ private val delete: (LogRecording) -> Unit
+): BaseListAdapter(diffCallback()) {
+
+ override fun createHolder(layoutInflater: LayoutInflater, parent: ViewGroup) =
+ RecordingViewHolder(
+ binding = ItemRecordingBinding.inflate(layoutInflater, parent, false),
+ click = click,
+ delete = delete,
+ )
+}
diff --git a/feature/recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/ui/fragment/RecordingsFragment.kt b/feature/recordings/list/src/main/kotlin/com/f0x1d/logfox/feature/recordings/list/presentation/ui/fragment/RecordingsFragment.kt
similarity index 77%
rename from feature/recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/ui/fragment/RecordingsFragment.kt
rename to feature/recordings/list/src/main/kotlin/com/f0x1d/logfox/feature/recordings/list/presentation/ui/fragment/RecordingsFragment.kt
index c04fab79..31973933 100644
--- a/feature/recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/ui/fragment/RecordingsFragment.kt
+++ b/feature/recordings/list/src/main/kotlin/com/f0x1d/logfox/feature/recordings/list/presentation/ui/fragment/RecordingsFragment.kt
@@ -1,4 +1,4 @@
-package com.f0x1d.logfox.feature.recordings.ui.fragment
+package com.f0x1d.logfox.feature.recordings.list.presentation.ui.fragment
import android.os.Bundle
import android.view.LayoutInflater
@@ -10,15 +10,14 @@ import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.f0x1d.logfox.arch.isHorizontalOrientation
-import com.f0x1d.logfox.arch.ui.fragment.BaseViewModelFragment
-import com.f0x1d.logfox.arch.viewmodel.Event
+import com.f0x1d.logfox.arch.presentation.ui.fragment.BaseFragment
import com.f0x1d.logfox.database.entity.LogRecording
-import com.f0x1d.logfox.feature.recordings.R
-import com.f0x1d.logfox.feature.recordings.adapter.RecordingsAdapter
-import com.f0x1d.logfox.feature.recordings.core.controller.RecordingState
-import com.f0x1d.logfox.feature.recordings.databinding.FragmentRecordingsBinding
-import com.f0x1d.logfox.feature.recordings.viewmodel.OpenRecording
-import com.f0x1d.logfox.feature.recordings.viewmodel.RecordingsViewModel
+import com.f0x1d.logfox.feature.recordings.api.data.RecordingState
+import com.f0x1d.logfox.feature.recordings.list.R
+import com.f0x1d.logfox.feature.recordings.list.databinding.FragmentRecordingsBinding
+import com.f0x1d.logfox.feature.recordings.list.presentation.RecordingsAction
+import com.f0x1d.logfox.feature.recordings.list.presentation.RecordingsViewModel
+import com.f0x1d.logfox.feature.recordings.list.presentation.adapter.RecordingsAdapter
import com.f0x1d.logfox.navigation.Directions
import com.f0x1d.logfox.strings.Strings
import com.f0x1d.logfox.ui.Icons
@@ -32,9 +31,9 @@ import dagger.hilt.android.AndroidEntryPoint
import dev.chrisbanes.insetter.applyInsetter
@AndroidEntryPoint
-class RecordingsFragment: BaseViewModelFragment() {
+class RecordingsFragment: BaseFragment() {
- override val viewModel by viewModels()
+ private val viewModel by viewModels