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() private val adapter = RecordingsAdapter( click = { @@ -101,19 +100,17 @@ class RecordingsFragment: BaseViewModelFragment + placeholderLayout.root.isVisible = state.recordings.isEmpty() - adapter.submitList(it) - } + adapter.submitList(state.recordings) - viewModel.recordingState.collectWithLifecycle { state -> recordFab.apply { - when (state) { + when (val recordingState = state.recordingState) { RecordingState.IDLE, RecordingState.SAVING -> { setImageResource(Icons.ic_recording) setDescription(Strings.record) - isEnabled = state == RecordingState.IDLE + isEnabled = recordingState == RecordingState.IDLE } RecordingState.RECORDING, RecordingState.PAUSED -> { @@ -127,7 +124,7 @@ class RecordingsFragment: BaseViewModelFragment { hide() } @@ -146,13 +143,12 @@ class RecordingsFragment: BaseViewModelFragment openDetails(event.recording) + viewModel.actions.collectWithLifecycle { action -> + when (action) { + is RecordingsAction.ShowSnackbar -> snackbar(action.text) + is RecordingsAction.OpenRecording -> openDetails(action.recording) + } } } diff --git a/feature/recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/ui/viewholder/RecordingViewHolder.kt b/feature/recordings/list/src/main/kotlin/com/f0x1d/logfox/feature/recordings/list/presentation/ui/viewholder/RecordingViewHolder.kt similarity index 76% rename from feature/recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/ui/viewholder/RecordingViewHolder.kt rename to feature/recordings/list/src/main/kotlin/com/f0x1d/logfox/feature/recordings/list/presentation/ui/viewholder/RecordingViewHolder.kt index 6e7df19a..f710457c 100644 --- a/feature/recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/ui/viewholder/RecordingViewHolder.kt +++ b/feature/recordings/list/src/main/kotlin/com/f0x1d/logfox/feature/recordings/list/presentation/ui/viewholder/RecordingViewHolder.kt @@ -1,8 +1,8 @@ -package com.f0x1d.logfox.feature.recordings.ui.viewholder +package com.f0x1d.logfox.feature.recordings.list.presentation.ui.viewholder -import com.f0x1d.logfox.arch.ui.viewholder.BaseViewHolder +import com.f0x1d.logfox.arch.presentation.ui.viewholder.BaseViewHolder import com.f0x1d.logfox.database.entity.LogRecording -import com.f0x1d.logfox.feature.recordings.databinding.ItemRecordingBinding +import com.f0x1d.logfox.feature.recordings.list.databinding.ItemRecordingBinding import java.util.Date class RecordingViewHolder( diff --git a/feature/recordings/src/main/res/layout/fragment_recordings.xml b/feature/recordings/list/src/main/res/layout/fragment_recordings.xml similarity index 100% rename from feature/recordings/src/main/res/layout/fragment_recordings.xml rename to feature/recordings/list/src/main/res/layout/fragment_recordings.xml diff --git a/feature/recordings/src/main/res/layout/item_recording.xml b/feature/recordings/list/src/main/res/layout/item_recording.xml similarity index 100% rename from feature/recordings/src/main/res/layout/item_recording.xml rename to feature/recordings/list/src/main/res/layout/item_recording.xml diff --git a/feature/recordings/src/main/res/layout/placeholder_recordings.xml b/feature/recordings/list/src/main/res/layout/placeholder_recordings.xml similarity index 100% rename from feature/recordings/src/main/res/layout/placeholder_recordings.xml rename to feature/recordings/list/src/main/res/layout/placeholder_recordings.xml diff --git a/feature/recordings/src/main/res/menu/recordings_menu.xml b/feature/recordings/list/src/main/res/menu/recordings_menu.xml similarity index 100% rename from feature/recordings/src/main/res/menu/recordings_menu.xml rename to feature/recordings/list/src/main/res/menu/recordings_menu.xml diff --git a/feature/recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/adapter/RecordingsAdapter.kt b/feature/recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/adapter/RecordingsAdapter.kt deleted file mode 100644 index f58e1e64..00000000 --- a/feature/recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/adapter/RecordingsAdapter.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.f0x1d.logfox.feature.recordings.adapter - -import android.view.LayoutInflater -import android.view.ViewGroup -import com.f0x1d.logfox.arch.adapter.BaseListAdapter -import com.f0x1d.logfox.database.entity.LogRecording -import com.f0x1d.logfox.feature.recordings.databinding.ItemRecordingBinding -import com.f0x1d.logfox.feature.recordings.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/viewmodel/RecordingsViewModel.kt b/feature/recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/viewmodel/RecordingsViewModel.kt deleted file mode 100644 index 1a40ded8..00000000 --- a/feature/recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/viewmodel/RecordingsViewModel.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.f0x1d.logfox.feature.recordings.viewmodel - -import android.app.Application -import com.f0x1d.logfox.arch.viewmodel.BaseViewModel -import com.f0x1d.logfox.arch.viewmodel.Event -import com.f0x1d.logfox.database.entity.LogRecording -import com.f0x1d.logfox.feature.recordings.core.controller.RecordingController -import com.f0x1d.logfox.feature.recordings.core.controller.RecordingState -import com.f0x1d.logfox.feature.recordings.core.repository.RecordingsRepository -import com.f0x1d.logfox.strings.Strings -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.distinctUntilChanged -import javax.inject.Inject - -@HiltViewModel -class RecordingsViewModel @Inject constructor( - private val recordingsRepository: RecordingsRepository, - private val recordingController: RecordingController, - application: Application, -): BaseViewModel(application) { - - val recordings = recordingsRepository.getAllAsFlow() - .distinctUntilChanged() - - val recordingState = recordingController.recordingState - - fun toggleStartStop() = launchCatching { - if (recordingState.value == RecordingState.IDLE) - recordingController.record() - else - recordingController.end().also { - sendEvent(OpenRecording(it)) - } - } - - fun togglePauseResume() = launchCatching { - if (recordingState.value == RecordingState.PAUSED) - recordingController.resume() - else - recordingController.pause() - } - - fun clearRecordings() = launchCatching { - recordingsRepository.clear() - } - - fun saveAll() = launchCatching { - snackbar(Strings.saving_logs) - recordingsRepository.saveAll().also { - sendEvent(OpenRecording(it)) - } - } - - fun delete(logRecording: LogRecording) = launchCatching { - recordingsRepository.delete(logRecording) - } -} - -data class OpenRecording( - val recording: LogRecording?, -) : Event diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts index f82706de..8673afbf 100644 --- a/feature/settings/build.gradle.kts +++ b/feature/settings/build.gradle.kts @@ -5,5 +5,5 @@ plugins { android.namespace = "com.f0x1d.logfox.feature.settings" dependencies { - + implementation(projects.feature.logging.api) } diff --git a/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/LoggingServiceDelegate.kt b/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/LoggingServiceDelegate.kt deleted file mode 100644 index 98dc57b8..00000000 --- a/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/LoggingServiceDelegate.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.f0x1d.logfox.feature.settings - -interface LoggingServiceDelegate { - fun restartLogging() -} diff --git a/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/IntArrayExt.kt b/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/presentation/IntArrayExt.kt similarity index 68% rename from feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/IntArrayExt.kt rename to feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/presentation/IntArrayExt.kt index 7cf54287..2b4fc9cb 100644 --- a/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/IntArrayExt.kt +++ b/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/presentation/IntArrayExt.kt @@ -1,4 +1,4 @@ -package com.f0x1d.logfox.feature.settings +package com.f0x1d.logfox.feature.settings.presentation import android.content.Context diff --git a/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsCrashesFragment.kt b/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/presentation/ui/fragment/SettingsCrashesFragment.kt similarity index 74% rename from feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsCrashesFragment.kt rename to feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/presentation/ui/fragment/SettingsCrashesFragment.kt index 5973dac2..1db81364 100644 --- a/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsCrashesFragment.kt +++ b/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/presentation/ui/fragment/SettingsCrashesFragment.kt @@ -1,8 +1,8 @@ -package com.f0x1d.logfox.feature.settings.ui.fragment +package com.f0x1d.logfox.feature.settings.presentation.ui.fragment import android.os.Bundle import com.f0x1d.logfox.feature.settings.R -import com.f0x1d.logfox.feature.settings.ui.fragment.base.BasePreferenceFragment +import com.f0x1d.logfox.feature.settings.presentation.ui.fragment.base.BasePreferenceFragment import com.f0x1d.logfox.strings.Strings import dagger.hilt.android.AndroidEntryPoint diff --git a/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsLinksFragment.kt b/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/presentation/ui/fragment/SettingsLinksFragment.kt similarity index 74% rename from feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsLinksFragment.kt rename to feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/presentation/ui/fragment/SettingsLinksFragment.kt index 527e569b..1a2a9c2f 100644 --- a/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsLinksFragment.kt +++ b/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/presentation/ui/fragment/SettingsLinksFragment.kt @@ -1,8 +1,8 @@ -package com.f0x1d.logfox.feature.settings.ui.fragment +package com.f0x1d.logfox.feature.settings.presentation.ui.fragment import android.os.Bundle import com.f0x1d.logfox.feature.settings.R -import com.f0x1d.logfox.feature.settings.ui.fragment.base.BasePreferenceFragment +import com.f0x1d.logfox.feature.settings.presentation.ui.fragment.base.BasePreferenceFragment import com.f0x1d.logfox.strings.Strings import dagger.hilt.android.AndroidEntryPoint diff --git a/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsMenuFragment.kt b/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/presentation/ui/fragment/SettingsMenuFragment.kt similarity index 77% rename from feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsMenuFragment.kt rename to feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/presentation/ui/fragment/SettingsMenuFragment.kt index 5aa43389..cd275bb1 100644 --- a/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsMenuFragment.kt +++ b/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/presentation/ui/fragment/SettingsMenuFragment.kt @@ -1,10 +1,13 @@ -package com.f0x1d.logfox.feature.settings.ui.fragment +package com.f0x1d.logfox.feature.settings.presentation.ui.fragment import android.os.Bundle import androidx.navigation.fragment.findNavController import androidx.preference.Preference +import com.f0x1d.logfox.arch.logs.timberLogFile +import com.f0x1d.logfox.arch.shareFileIntent +import com.f0x1d.logfox.feature.settings.BuildConfig import com.f0x1d.logfox.feature.settings.R -import com.f0x1d.logfox.feature.settings.ui.fragment.base.BasePreferenceFragment +import com.f0x1d.logfox.feature.settings.presentation.ui.fragment.base.BasePreferenceFragment import com.f0x1d.logfox.navigation.Directions import dagger.hilt.android.AndroidEntryPoint @@ -30,15 +33,23 @@ class SettingsMenuFragment: BasePreferenceFragment() { return@setOnPreferenceClickListener true } + findPreference("pref_settings_links")?.setOnPreferenceClickListener { + findNavController().navigate(Directions.action_settingsMenuFragment_to_settingsLinksFragment) + return@setOnPreferenceClickListener true + } findPreference("pref_settings_app_version")?.apply { val packageManager = requireContext().packageManager val packageInfo = packageManager.getPackageInfo(requireContext().packageName, 0) title = "${packageInfo.versionName} (${packageInfo.versionCode})" } - findPreference("pref_settings_links")?.setOnPreferenceClickListener { - findNavController().navigate(Directions.action_settingsMenuFragment_to_settingsLinksFragment) - return@setOnPreferenceClickListener true + findPreference("pref_settings_share_logs")?.apply { + isVisible = BuildConfig.DEBUG + + setOnPreferenceClickListener { + requireContext().shareFileIntent(requireContext().timberLogFile) + return@setOnPreferenceClickListener true + } } } } diff --git a/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsNotificationsFragment.kt b/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/presentation/ui/fragment/SettingsNotificationsFragment.kt similarity index 93% rename from feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsNotificationsFragment.kt rename to feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/presentation/ui/fragment/SettingsNotificationsFragment.kt index b5f0858b..76b4f651 100644 --- a/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsNotificationsFragment.kt +++ b/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/presentation/ui/fragment/SettingsNotificationsFragment.kt @@ -1,4 +1,4 @@ -package com.f0x1d.logfox.feature.settings.ui.fragment +package com.f0x1d.logfox.feature.settings.presentation.ui.fragment import android.annotation.SuppressLint import android.content.Intent @@ -9,7 +9,7 @@ import com.f0x1d.logfox.arch.LOGGING_STATUS_CHANNEL_ID import com.f0x1d.logfox.arch.hasNotificationsPermission import com.f0x1d.logfox.arch.notificationsChannelsAvailable import com.f0x1d.logfox.feature.settings.R -import com.f0x1d.logfox.feature.settings.ui.fragment.base.BasePreferenceFragment +import com.f0x1d.logfox.feature.settings.presentation.ui.fragment.base.BasePreferenceFragment import com.f0x1d.logfox.strings.Strings import dagger.hilt.android.AndroidEntryPoint diff --git a/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsServiceFragment.kt b/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/presentation/ui/fragment/SettingsServiceFragment.kt similarity index 90% rename from feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsServiceFragment.kt rename to feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/presentation/ui/fragment/SettingsServiceFragment.kt index f1c76cd6..5ac5e318 100644 --- a/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsServiceFragment.kt +++ b/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/presentation/ui/fragment/SettingsServiceFragment.kt @@ -1,4 +1,4 @@ -package com.f0x1d.logfox.feature.settings.ui.fragment +package com.f0x1d.logfox.feature.settings.presentation.ui.fragment import android.os.Bundle import androidx.lifecycle.lifecycleScope @@ -6,10 +6,10 @@ import androidx.preference.Preference import androidx.preference.SwitchPreferenceCompat import com.f0x1d.logfox.arch.isAtLeastAndroid13 import com.f0x1d.logfox.arch.toast -import com.f0x1d.logfox.feature.settings.LoggingServiceDelegate +import com.f0x1d.logfox.feature.logging.api.presentation.LoggingServiceDelegate import com.f0x1d.logfox.feature.settings.R -import com.f0x1d.logfox.feature.settings.fillWithStrings -import com.f0x1d.logfox.feature.settings.ui.fragment.base.BasePreferenceFragment +import com.f0x1d.logfox.feature.settings.presentation.fillWithStrings +import com.f0x1d.logfox.feature.settings.presentation.ui.fragment.base.BasePreferenceFragment import com.f0x1d.logfox.preferences.shared.AppPreferences import com.f0x1d.logfox.strings.Strings import com.f0x1d.logfox.terminals.base.Terminal @@ -33,7 +33,7 @@ class SettingsServiceFragment: BasePreferenceFragment() { lateinit var terminals: Array @Inject - lateinit var loggingService: LoggingServiceDelegate + lateinit var loggingServiceDelegate: LoggingServiceDelegate override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.settings_service) @@ -94,7 +94,7 @@ class SettingsServiceFragment: BasePreferenceFragment() { } private fun restartLogging() { - loggingService.restartLogging() + loggingServiceDelegate.restartLogging() } private fun askAboutNewTerminalRestart() { diff --git a/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsUIFragment.kt b/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/presentation/ui/fragment/SettingsUIFragment.kt similarity index 97% rename from feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsUIFragment.kt rename to feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/presentation/ui/fragment/SettingsUIFragment.kt index 045ec88a..d232ef1f 100644 --- a/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/SettingsUIFragment.kt +++ b/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/presentation/ui/fragment/SettingsUIFragment.kt @@ -1,4 +1,4 @@ -package com.f0x1d.logfox.feature.settings.ui.fragment +package com.f0x1d.logfox.feature.settings.presentation.ui.fragment import android.os.Bundle import android.text.InputType @@ -7,8 +7,8 @@ import androidx.preference.Preference import com.f0x1d.logfox.arch.catchingNotNumber import com.f0x1d.logfox.arch.monetAvailable import com.f0x1d.logfox.feature.settings.R -import com.f0x1d.logfox.feature.settings.fillWithStrings -import com.f0x1d.logfox.feature.settings.ui.fragment.base.BasePreferenceFragment +import com.f0x1d.logfox.feature.settings.presentation.fillWithStrings +import com.f0x1d.logfox.feature.settings.presentation.ui.fragment.base.BasePreferenceFragment import com.f0x1d.logfox.preferences.shared.AppPreferences import com.f0x1d.logfox.strings.Strings import com.f0x1d.logfox.ui.Icons diff --git a/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/base/BasePreferenceFragment.kt b/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/presentation/ui/fragment/base/BasePreferenceFragment.kt similarity index 88% rename from feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/base/BasePreferenceFragment.kt rename to feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/presentation/ui/fragment/base/BasePreferenceFragment.kt index c6446c30..993dd47d 100644 --- a/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/ui/fragment/base/BasePreferenceFragment.kt +++ b/feature/settings/src/main/kotlin/com/f0x1d/logfox/feature/settings/presentation/ui/fragment/base/BasePreferenceFragment.kt @@ -1,10 +1,10 @@ -package com.f0x1d.logfox.feature.settings.ui.fragment.base +package com.f0x1d.logfox.feature.settings.presentation.ui.fragment.base import android.os.Bundle import android.view.View import androidx.preference.PreferenceFragmentCompat import com.f0x1d.logfox.arch.isHorizontalOrientation -import com.f0x1d.logfox.arch.ui.base.SimpleFragmentLifecycleOwner +import com.f0x1d.logfox.arch.presentation.ui.base.SimpleFragmentLifecycleOwner import com.f0x1d.logfox.feature.settings.R import com.f0x1d.logfox.strings.Strings import com.f0x1d.logfox.ui.view.setupBackButtonForNavController diff --git a/feature/settings/src/main/res/xml/settings_menu.xml b/feature/settings/src/main/res/xml/settings_menu.xml index 59d02d49..6be97bf8 100644 --- a/feature/settings/src/main/res/xml/settings_menu.xml +++ b/feature/settings/src/main/res/xml/settings_menu.xml @@ -34,6 +34,9 @@ android:key="pref_settings_app_version" android:icon="@drawable/ic_settings_info" android:selectable="false" /> + diff --git a/feature/setup/src/main/kotlin/com/f0x1d/logfox/feature/setup/presentation/SetupAction.kt b/feature/setup/src/main/kotlin/com/f0x1d/logfox/feature/setup/presentation/SetupAction.kt new file mode 100644 index 00000000..f9a44129 --- /dev/null +++ b/feature/setup/src/main/kotlin/com/f0x1d/logfox/feature/setup/presentation/SetupAction.kt @@ -0,0 +1,7 @@ +package com.f0x1d.logfox.feature.setup.presentation + +import androidx.annotation.StringRes + +sealed interface SetupAction { + data class ShowSnackbar(@StringRes val textResId: Int) : SetupAction +} diff --git a/feature/setup/src/main/kotlin/com/f0x1d/logfox/feature/setup/presentation/SetupState.kt b/feature/setup/src/main/kotlin/com/f0x1d/logfox/feature/setup/presentation/SetupState.kt new file mode 100644 index 00000000..b681c489 --- /dev/null +++ b/feature/setup/src/main/kotlin/com/f0x1d/logfox/feature/setup/presentation/SetupState.kt @@ -0,0 +1,6 @@ +package com.f0x1d.logfox.feature.setup.presentation + +data class SetupState( + val showAdbDialog: Boolean = false, + val adbCommand: String = "", +) diff --git a/feature/setup/src/main/kotlin/com/f0x1d/logfox/feature/setup/viewmodel/SetupViewModel.kt b/feature/setup/src/main/kotlin/com/f0x1d/logfox/feature/setup/presentation/SetupViewModel.kt similarity index 79% rename from feature/setup/src/main/kotlin/com/f0x1d/logfox/feature/setup/viewmodel/SetupViewModel.kt rename to feature/setup/src/main/kotlin/com/f0x1d/logfox/feature/setup/presentation/SetupViewModel.kt index bd4d9ace..23d5d382 100644 --- a/feature/setup/src/main/kotlin/com/f0x1d/logfox/feature/setup/viewmodel/SetupViewModel.kt +++ b/feature/setup/src/main/kotlin/com/f0x1d/logfox/feature/setup/presentation/SetupViewModel.kt @@ -1,12 +1,11 @@ -package com.f0x1d.logfox.feature.setup.viewmodel +package com.f0x1d.logfox.feature.setup.presentation import android.Manifest import android.app.Application import com.f0x1d.logfox.arch.copyText import com.f0x1d.logfox.arch.hardRestartApp import com.f0x1d.logfox.arch.hasPermissionToReadLogs -import com.f0x1d.logfox.arch.viewmodel.BaseStateViewModel -import com.f0x1d.logfox.feature.setup.ui.fragment.setup.compose.SetupScreenState +import com.f0x1d.logfox.arch.viewmodel.BaseViewModel import com.f0x1d.logfox.preferences.shared.AppPreferences import com.f0x1d.logfox.strings.Strings import com.f0x1d.logfox.terminals.DefaultTerminal @@ -21,11 +20,10 @@ class SetupViewModel @Inject constructor( private val rootTerminal: RootTerminal, private val shizukuTerminal: ShizukuTerminal, application: Application, -): BaseStateViewModel( - initialStateProvider = { SetupScreenState() }, +): BaseViewModel( + initialStateProvider = { SetupState() }, application = application, ) { - private val adbCommand get() = "adb shell ${command.joinToString(" ")}" private val command get() = arrayOf("pm", "grant", ctx.packageName, Manifest.permission.READ_LOGS) @@ -36,14 +34,14 @@ class SetupViewModel @Inject constructor( rootTerminal.executeNow(*command) checkPermission() } else - snackbar(Strings.no_root) + sendAction(SetupAction.ShowSnackbar(Strings.no_root)) } fun adb() = launchCatching { if (ctx.hasPermissionToReadLogs) gotPermission() else { - state { + reduce { copy( showAdbDialog = true, adbCommand = this@SetupViewModel.adbCommand, @@ -60,20 +58,20 @@ class SetupViewModel @Inject constructor( if (shizukuTerminal.isSupported() && shizukuTerminal.executeNow(*command).isSuccessful) gotPermission() else - snackbar(Strings.shizuku_error) + sendAction(SetupAction.ShowSnackbar(Strings.shizuku_error)) } fun checkPermission() = if (ctx.hasPermissionToReadLogs) gotPermission() else - snackbar(Strings.no_permission_detected) + sendAction(SetupAction.ShowSnackbar(Strings.no_permission_detected)) fun copyCommand() { ctx.copyText(adbCommand) - snackbar(Strings.text_copied) + sendAction(SetupAction.ShowSnackbar(Strings.text_copied)) } - fun closeAdbDialog() = state { + fun closeAdbDialog() = reduce { copy(showAdbDialog = false) } diff --git a/feature/setup/src/main/kotlin/com/f0x1d/logfox/feature/setup/presentation/ui/SetupFragment.kt b/feature/setup/src/main/kotlin/com/f0x1d/logfox/feature/setup/presentation/ui/SetupFragment.kt new file mode 100644 index 00000000..0f64b24a --- /dev/null +++ b/feature/setup/src/main/kotlin/com/f0x1d/logfox/feature/setup/presentation/ui/SetupFragment.kt @@ -0,0 +1,58 @@ +package com.f0x1d.logfox.feature.setup.presentation.ui + +import android.os.Bundle +import android.view.View +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.fragment.app.viewModels +import com.f0x1d.logfox.arch.presentation.ui.fragment.compose.BaseComposeFragment +import com.f0x1d.logfox.feature.setup.presentation.SetupAction +import com.f0x1d.logfox.feature.setup.presentation.SetupViewModel +import com.f0x1d.logfox.feature.setup.presentation.ui.compose.SetupScreenContent +import com.f0x1d.logfox.ui.compose.theme.LogFoxTheme +import dagger.hilt.android.AndroidEntryPoint +import dev.chrisbanes.insetter.applyInsetter + +@AndroidEntryPoint +class SetupFragment : BaseComposeFragment() { + + private val viewModel by viewModels() + + private val listener: SetupScreenListener by lazy { + SetupScreenListener( + onRootClick = viewModel::root, + onAdbClick = viewModel::adb, + onShizukuClick = viewModel::shizuku, + closeAdbDialog = viewModel::closeAdbDialog, + checkPermission = viewModel::checkPermission, + copyCommand = viewModel::copyCommand, + ) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel.actions.collectWithLifecycle { action -> + when (action) { + is SetupAction.ShowSnackbar -> snackbar(action.textResId).apply { + this.view.applyInsetter { + type(navigationBars = true) { margin() } + } + } + } + } + } + + @Composable + override fun Content() { + LogFoxTheme { + val state by viewModel.state.collectAsState() + + SetupScreenContent( + state = state, + listener = listener, + ) + } + } +} diff --git a/feature/setup/src/main/kotlin/com/f0x1d/logfox/feature/setup/ui/fragment/setup/compose/SetupScreenState.kt b/feature/setup/src/main/kotlin/com/f0x1d/logfox/feature/setup/presentation/ui/SetupScreenListener.kt similarity index 74% rename from feature/setup/src/main/kotlin/com/f0x1d/logfox/feature/setup/ui/fragment/setup/compose/SetupScreenState.kt rename to feature/setup/src/main/kotlin/com/f0x1d/logfox/feature/setup/presentation/ui/SetupScreenListener.kt index 25ecf71e..61b313cf 100644 --- a/feature/setup/src/main/kotlin/com/f0x1d/logfox/feature/setup/ui/fragment/setup/compose/SetupScreenState.kt +++ b/feature/setup/src/main/kotlin/com/f0x1d/logfox/feature/setup/presentation/ui/SetupScreenListener.kt @@ -1,12 +1,7 @@ -package com.f0x1d.logfox.feature.setup.ui.fragment.setup.compose +package com.f0x1d.logfox.feature.setup.presentation.ui import androidx.compose.runtime.Immutable -data class SetupScreenState( - val showAdbDialog: Boolean = false, - val adbCommand: String = "", -) - @Immutable data class SetupScreenListener( val onRootClick: () -> Unit, diff --git a/feature/setup/src/main/kotlin/com/f0x1d/logfox/feature/setup/ui/fragment/setup/compose/SetupScreenContent.kt b/feature/setup/src/main/kotlin/com/f0x1d/logfox/feature/setup/presentation/ui/compose/SetupScreenContent.kt similarity index 93% rename from feature/setup/src/main/kotlin/com/f0x1d/logfox/feature/setup/ui/fragment/setup/compose/SetupScreenContent.kt rename to feature/setup/src/main/kotlin/com/f0x1d/logfox/feature/setup/presentation/ui/compose/SetupScreenContent.kt index ac731ac0..34107fbc 100644 --- a/feature/setup/src/main/kotlin/com/f0x1d/logfox/feature/setup/ui/fragment/setup/compose/SetupScreenContent.kt +++ b/feature/setup/src/main/kotlin/com/f0x1d/logfox/feature/setup/presentation/ui/compose/SetupScreenContent.kt @@ -1,4 +1,4 @@ -package com.f0x1d.logfox.feature.setup.ui.fragment.setup.compose +package com.f0x1d.logfox.feature.setup.presentation.ui.compose import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -21,6 +21,9 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import com.f0x1d.logfox.feature.setup.presentation.SetupState +import com.f0x1d.logfox.feature.setup.presentation.ui.MockSetupScreenListener +import com.f0x1d.logfox.feature.setup.presentation.ui.SetupScreenListener import com.f0x1d.logfox.strings.Strings import com.f0x1d.logfox.ui.Icons import com.f0x1d.logfox.ui.compose.component.button.RichButton @@ -30,7 +33,7 @@ import com.f0x1d.logfox.ui.compose.theme.LogFoxTheme @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun SetupScreenContent( - state: SetupScreenState = SetupScreenState(), + state: SetupState = SetupState(), listener: SetupScreenListener = MockSetupScreenListener, ) { Scaffold( @@ -158,7 +161,7 @@ private fun SetupScreenContentPreview() { private fun SetupScreenContentWithDialogPreview() { LogFoxTheme { SetupScreenContent( - state = SetupScreenState( + state = SetupState( showAdbDialog = true, adbCommand = "HESOYAM", ) diff --git a/feature/setup/src/main/kotlin/com/f0x1d/logfox/feature/setup/ui/fragment/setup/SetupFragment.kt b/feature/setup/src/main/kotlin/com/f0x1d/logfox/feature/setup/ui/fragment/setup/SetupFragment.kt deleted file mode 100644 index 342bc1e8..00000000 --- a/feature/setup/src/main/kotlin/com/f0x1d/logfox/feature/setup/ui/fragment/setup/SetupFragment.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.f0x1d.logfox.feature.setup.ui.fragment.setup - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.fragment.app.viewModels -import com.f0x1d.logfox.arch.ui.fragment.compose.BaseComposeViewModelFragment -import com.f0x1d.logfox.feature.setup.ui.fragment.setup.compose.SetupScreenContent -import com.f0x1d.logfox.feature.setup.ui.fragment.setup.compose.SetupScreenListener -import com.f0x1d.logfox.feature.setup.viewmodel.SetupViewModel -import com.f0x1d.logfox.ui.compose.theme.LogFoxTheme -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class SetupFragment: BaseComposeViewModelFragment() { - - override val viewModel by viewModels() - - private val listener by lazy { - SetupScreenListener( - onRootClick = viewModel::root, - onAdbClick = viewModel::adb, - onShizukuClick = viewModel::shizuku, - closeAdbDialog = viewModel::closeAdbDialog, - checkPermission = viewModel::checkPermission, - copyCommand = viewModel::copyCommand, - ) - } - - @Composable - override fun Content() { - LogFoxTheme { - val state by viewModel.uiState.collectAsState() - - SetupScreenContent( - state = state, - listener = listener, - ) - } - } -} diff --git a/feature/setup/src/test/kotlin/com/f0x1d/logfox/setup/compose/SetupScreenContentTest.kt b/feature/setup/src/test/kotlin/com/f0x1d/logfox/feature/setup/presentation/ui/compose/SetupScreenContentTest.kt similarity index 90% rename from feature/setup/src/test/kotlin/com/f0x1d/logfox/setup/compose/SetupScreenContentTest.kt rename to feature/setup/src/test/kotlin/com/f0x1d/logfox/feature/setup/presentation/ui/compose/SetupScreenContentTest.kt index fbbaceaf..b4172894 100644 --- a/feature/setup/src/test/kotlin/com/f0x1d/logfox/setup/compose/SetupScreenContentTest.kt +++ b/feature/setup/src/test/kotlin/com/f0x1d/logfox/feature/setup/presentation/ui/compose/SetupScreenContentTest.kt @@ -1,4 +1,4 @@ -package com.f0x1d.logfox.setup.compose +package com.f0x1d.logfox.feature.setup.presentation.ui.compose import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -6,11 +6,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import com.f0x1d.logfox.core.tests.ScreenshotTest import com.f0x1d.logfox.core.tests.compose.clickOn -import com.f0x1d.logfox.feature.setup.ui.fragment.setup.compose.MockSetupScreenListener -import com.f0x1d.logfox.feature.setup.ui.fragment.setup.compose.SetupAdbButtonTestTag -import com.f0x1d.logfox.feature.setup.ui.fragment.setup.compose.SetupAdbDialogTestTag -import com.f0x1d.logfox.feature.setup.ui.fragment.setup.compose.SetupScreenContent -import com.f0x1d.logfox.feature.setup.ui.fragment.setup.compose.SetupScreenState +import com.f0x1d.logfox.feature.setup.presentation.SetupState +import com.f0x1d.logfox.feature.setup.presentation.ui.MockSetupScreenListener import com.f0x1d.logfox.ui.compose.theme.LogFoxTheme import org.junit.Test @@ -36,7 +33,7 @@ class SetupScreenContentTest : ScreenshotTest() { ) { LogFoxTheme { SetupScreenContent( - state = SetupScreenState( + state = SetupState( showAdbDialog = true, adbCommand = OG_BUDA_ISKS, ), @@ -50,7 +47,7 @@ class SetupScreenContentTest : ScreenshotTest() { whatToCapture = { SetupAdbDialogTestTag.node() }, ) { var state by remember { - mutableStateOf(SetupScreenState()) + mutableStateOf(SetupState()) } LogFoxTheme { diff --git a/feature/setup/src/test/screenshots/com.f0x1d.logfox.feature.setup.presentation.ui.compose.SetupScreenContentTest.shouldOpenAdbDialogOnSetupScreenContent.png b/feature/setup/src/test/screenshots/com.f0x1d.logfox.feature.setup.presentation.ui.compose.SetupScreenContentTest.shouldOpenAdbDialogOnSetupScreenContent.png new file mode 100644 index 00000000..9f0923be Binary files /dev/null and b/feature/setup/src/test/screenshots/com.f0x1d.logfox.feature.setup.presentation.ui.compose.SetupScreenContentTest.shouldOpenAdbDialogOnSetupScreenContent.png differ diff --git a/feature/setup/src/test/screenshots/com.f0x1d.logfox.feature.setup.presentation.ui.compose.SetupScreenContentTest.shouldShowAdbDialogOnSetupScreenContent.png b/feature/setup/src/test/screenshots/com.f0x1d.logfox.feature.setup.presentation.ui.compose.SetupScreenContentTest.shouldShowAdbDialogOnSetupScreenContent.png new file mode 100644 index 00000000..ab173cee Binary files /dev/null and b/feature/setup/src/test/screenshots/com.f0x1d.logfox.feature.setup.presentation.ui.compose.SetupScreenContentTest.shouldShowAdbDialogOnSetupScreenContent.png differ diff --git a/feature/setup/src/test/screenshots/com.f0x1d.logfox.setup.compose.SetupScreenContentTest.shouldShowDarkSetupScreenContent.png b/feature/setup/src/test/screenshots/com.f0x1d.logfox.feature.setup.presentation.ui.compose.SetupScreenContentTest.shouldShowDarkSetupScreenContent.png similarity index 94% rename from feature/setup/src/test/screenshots/com.f0x1d.logfox.setup.compose.SetupScreenContentTest.shouldShowDarkSetupScreenContent.png rename to feature/setup/src/test/screenshots/com.f0x1d.logfox.feature.setup.presentation.ui.compose.SetupScreenContentTest.shouldShowDarkSetupScreenContent.png index 31261d65..56e00491 100644 Binary files a/feature/setup/src/test/screenshots/com.f0x1d.logfox.setup.compose.SetupScreenContentTest.shouldShowDarkSetupScreenContent.png and b/feature/setup/src/test/screenshots/com.f0x1d.logfox.feature.setup.presentation.ui.compose.SetupScreenContentTest.shouldShowDarkSetupScreenContent.png differ diff --git a/feature/setup/src/test/screenshots/com.f0x1d.logfox.feature.setup.presentation.ui.compose.SetupScreenContentTest.shouldShowSetupScreenContent.png b/feature/setup/src/test/screenshots/com.f0x1d.logfox.feature.setup.presentation.ui.compose.SetupScreenContentTest.shouldShowSetupScreenContent.png new file mode 100644 index 00000000..d968059d Binary files /dev/null and b/feature/setup/src/test/screenshots/com.f0x1d.logfox.feature.setup.presentation.ui.compose.SetupScreenContentTest.shouldShowSetupScreenContent.png differ diff --git a/feature/setup/src/test/screenshots/com.f0x1d.logfox.setup.compose.SetupScreenContentTest.shouldOpenAdbDialogOnSetupScreenContent.png b/feature/setup/src/test/screenshots/com.f0x1d.logfox.setup.compose.SetupScreenContentTest.shouldOpenAdbDialogOnSetupScreenContent.png deleted file mode 100644 index 87f8cfb9..00000000 Binary files a/feature/setup/src/test/screenshots/com.f0x1d.logfox.setup.compose.SetupScreenContentTest.shouldOpenAdbDialogOnSetupScreenContent.png and /dev/null differ diff --git a/feature/setup/src/test/screenshots/com.f0x1d.logfox.setup.compose.SetupScreenContentTest.shouldShowAdbDialogOnSetupScreenContent.png b/feature/setup/src/test/screenshots/com.f0x1d.logfox.setup.compose.SetupScreenContentTest.shouldShowAdbDialogOnSetupScreenContent.png deleted file mode 100644 index 87cfc393..00000000 Binary files a/feature/setup/src/test/screenshots/com.f0x1d.logfox.setup.compose.SetupScreenContentTest.shouldShowAdbDialogOnSetupScreenContent.png and /dev/null differ diff --git a/feature/setup/src/test/screenshots/com.f0x1d.logfox.setup.compose.SetupScreenContentTest.shouldShowSetupScreenContent.png b/feature/setup/src/test/screenshots/com.f0x1d.logfox.setup.compose.SetupScreenContentTest.shouldShowSetupScreenContent.png deleted file mode 100644 index 35eaa09c..00000000 Binary files a/feature/setup/src/test/screenshots/com.f0x1d.logfox.setup.compose.SetupScreenContentTest.shouldShowSetupScreenContent.png and /dev/null differ diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c2519452..b75984ca 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,28 +1,28 @@ [versions] -compileSdk = "34" +compileSdk = "35" minSdk = "24" -targetSdk = "34" +targetSdk = "35" -kotlin = "2.0.0" +kotlin = "2.0.21" kotlinx-immutable-collections = "0.3.7" -androidGradlePlugin = "8.5.2" -ksp = "2.0.0-1.0.22" -hilt = "2.49" +androidGradlePlugin = "8.7.2" +ksp = "2.0.21-1.0.26" +hilt = "2.51.1" androidx-appcompat = "1.7.0" -androidx-constraintlayout = "2.1.4" -androidx-core = "1.13.1" -androidx-collection = "1.4.3" -androidx-fragment = "1.8.2" -androidx-activity = "1.9.1" +androidx-constraintlayout = "2.2.0" +androidx-core = "1.15.0" +androidx-collection = "1.4.5" +androidx-fragment = "1.8.5" +androidx-activity = "1.9.3" androidx-hilt-navigation-fragment = "1.2.0" -androidx-lifecycle = "2.8.4" -androidx-navigation = "2.7.7" +androidx-lifecycle = "2.8.7" +androidx-navigation = "2.8.3" androidx-preference = "1.2.1" androidx-recyclerview = "1.3.2" androidx-room = "2.6.1" -androidx-compose = "1.6.8" -androidx-compose-material3 = "1.2.1" +androidx-compose = "1.7.5" +androidx-compose-material3 = "1.3.1" insetter = "0.6.1" shizuku = "13.1.4" viewpump = "2.1.1" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b012c669..3c8ae094 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed May 01 00:57:48 MSK 2024 +#Fri Oct 11 20:56:31 MSK 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle.kts b/settings.gradle.kts index 8c21cfe5..cc4e9cec 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -15,25 +15,40 @@ dependencyResolutionManagement { } } +rootProject.name = "LogFox" + enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") include( ":app", - ":data", + ":shared", ":strings", ) -private val modulesDirectories = setOf("core", "feature") -private val submoduleNameRegex = "^[A-Za-z0-9\\-_]+\$".toRegex() +includeRecursive(File("core")) +includeRecursive(File("feature")) + +private fun includeRecursive( + directory: File, + parentDirectoriesNames: List = listOf(directory.name), +) { + fun File.isModule(): Boolean = File(this, "build.gradle.kts").isFile -requireNotNull(rootDir.listFiles()).filter { file -> - file.isDirectory && file.name in modulesDirectories -}.forEach { file -> - val modules = requireNotNull(file.listFiles()) + if (directory.isModule()) { + val moduleName = parentDirectoriesNames.joinToString( + prefix = ":", + separator = ":", + ) - modules.filter(File::isDirectory).forEach { moduleFile -> - if (submoduleNameRegex.matches(moduleFile.name)) { - include(":${file.name}:${moduleFile.name}") - } + include(moduleName) + } else { + directory + .listFiles() + ?.forEach { file -> + includeRecursive( + directory = file, + parentDirectoriesNames = parentDirectoriesNames + file.name, + ) + } } } diff --git a/feature/logging/.gitignore b/shared/.gitignore similarity index 100% rename from feature/logging/.gitignore rename to shared/.gitignore diff --git a/data/build.gradle.kts b/shared/build.gradle.kts similarity index 82% rename from data/build.gradle.kts rename to shared/build.gradle.kts index 1809be4b..20b336de 100644 --- a/data/build.gradle.kts +++ b/shared/build.gradle.kts @@ -6,4 +6,6 @@ android.namespace = "com.f0x1d.logfox.model" dependencies { implementation(libs.bundles.androidx) + + implementation(libs.timber) } diff --git a/data/src/main/kotlin/com/f0x1d/logfox/model/Device.kt b/shared/src/main/kotlin/com/f0x1d/logfox/model/Device.kt similarity index 100% rename from data/src/main/kotlin/com/f0x1d/logfox/model/Device.kt rename to shared/src/main/kotlin/com/f0x1d/logfox/model/Device.kt diff --git a/data/src/main/kotlin/com/f0x1d/logfox/model/Identifiable.kt b/shared/src/main/kotlin/com/f0x1d/logfox/model/Identifiable.kt similarity index 90% rename from data/src/main/kotlin/com/f0x1d/logfox/model/Identifiable.kt rename to shared/src/main/kotlin/com/f0x1d/logfox/model/Identifiable.kt index aecfa19a..6cd026a1 100644 --- a/data/src/main/kotlin/com/f0x1d/logfox/model/Identifiable.kt +++ b/shared/src/main/kotlin/com/f0x1d/logfox/model/Identifiable.kt @@ -2,7 +2,6 @@ package com.f0x1d.logfox.model import android.annotation.SuppressLint import androidx.recyclerview.widget.DiffUtil -import com.f0x1d.logfox.model.logline.LogLine interface Identifiable { val id: Any diff --git a/data/src/main/kotlin/com/f0x1d/logfox/model/UIDS.kt b/shared/src/main/kotlin/com/f0x1d/logfox/model/UIDS.kt similarity index 100% rename from data/src/main/kotlin/com/f0x1d/logfox/model/UIDS.kt rename to shared/src/main/kotlin/com/f0x1d/logfox/model/UIDS.kt diff --git a/data/src/main/kotlin/com/f0x1d/logfox/model/exception/TerminalNotSupportedException.kt b/shared/src/main/kotlin/com/f0x1d/logfox/model/exception/TerminalNotSupportedException.kt similarity index 100% rename from data/src/main/kotlin/com/f0x1d/logfox/model/exception/TerminalNotSupportedException.kt rename to shared/src/main/kotlin/com/f0x1d/logfox/model/exception/TerminalNotSupportedException.kt diff --git a/data/src/main/kotlin/com/f0x1d/logfox/model/logline/LogLine.kt b/shared/src/main/kotlin/com/f0x1d/logfox/model/logline/LogLine.kt similarity index 84% rename from data/src/main/kotlin/com/f0x1d/logfox/model/logline/LogLine.kt rename to shared/src/main/kotlin/com/f0x1d/logfox/model/logline/LogLine.kt index 146703fe..01f8afcb 100644 --- a/data/src/main/kotlin/com/f0x1d/logfox/model/logline/LogLine.kt +++ b/shared/src/main/kotlin/com/f0x1d/logfox/model/logline/LogLine.kt @@ -7,13 +7,13 @@ import java.util.Date data class LogLine( override val id: Long, - val dateAndTime: Long = System.currentTimeMillis(), - val uid: String = "", - val pid: String = "", - val tid: String = "", - val packageName: String? = null, - val level: LogLevel = LogLevel.INFO, - val tag: String = "", + val dateAndTime: Long, + val uid: String, + val pid: String, + val tid: String, + val packageName: String?, + val level: LogLevel, + val tag: String, val content: String, val originalContent: String, ) : Identifiable { diff --git a/data/src/main/kotlin/com/f0x1d/logfox/model/logline/LogLineExt.kt b/shared/src/main/kotlin/com/f0x1d/logfox/model/logline/LogLineExt.kt similarity index 87% rename from data/src/main/kotlin/com/f0x1d/logfox/model/logline/LogLineExt.kt rename to shared/src/main/kotlin/com/f0x1d/logfox/model/logline/LogLineExt.kt index c6272ed1..edf96b73 100644 --- a/data/src/main/kotlin/com/f0x1d/logfox/model/logline/LogLineExt.kt +++ b/shared/src/main/kotlin/com/f0x1d/logfox/model/logline/LogLineExt.kt @@ -3,6 +3,7 @@ package com.f0x1d.logfox.model.logline import android.content.Context import androidx.collection.LruCache import com.f0x1d.logfox.model.UIDS_MAPPINGS +import timber.log.Timber private val logRegex = "(.{14}) (.{5,}?) (.{1,5}) (.{1,5}) (.) (.+?): (.+)".toRegex() // time, uid, pid, tid, level, tag, message @@ -15,7 +16,11 @@ fun LogLine( line: String, context: Context ) = runCatching { - logRegex.find(line.trim())?.run { + logRegex.find(line.trim()).also { matchResult -> + if (matchResult == null) { + Timber.d("matchResult is null for $line") + } + }?.run { val uid = groupValues[2].replace(" ", "") val integerUid = uid.toIntOrNull() ?: UIDS_MAPPINGS[uid] ?: uidRegex.find(uid)?.run { 100_000 * groupValues[1].toInt() + 10_000 + groupValues[2].toInt() @@ -48,6 +53,8 @@ fun LogLine( originalContent = groupValues[0], ) } +}.onFailure { th -> + Timber.e("error while parsing", th) }.getOrNull() private fun mapLevel(level: String) = LogLevel.entries.find { diff --git a/data/src/main/kotlin/com/f0x1d/logfox/model/preferences/ShowLogValues.kt b/shared/src/main/kotlin/com/f0x1d/logfox/model/preferences/ShowLogValues.kt similarity index 100% rename from data/src/main/kotlin/com/f0x1d/logfox/model/preferences/ShowLogValues.kt rename to shared/src/main/kotlin/com/f0x1d/logfox/model/preferences/ShowLogValues.kt diff --git a/strings/src/main/res/values-ru/strings.xml b/strings/src/main/res/values-ru/strings.xml index 7ca76ee6..32e79969 100644 --- a/strings/src/main/res/values-ru/strings.xml +++ b/strings/src/main/res/values-ru/strings.xml @@ -157,4 +157,5 @@ Черный список Добавить в черный список Убрать из черного списка + Поделиться логами diff --git a/strings/src/main/res/values/strings.xml b/strings/src/main/res/values/strings.xml index 9ff9834a..786cbfa4 100644 --- a/strings/src/main/res/values/strings.xml +++ b/strings/src/main/res/values/strings.xml @@ -168,4 +168,5 @@ Remove from blacklist Are you sure want to add this app to blacklist? LogFox does not observe crashes for blacklisted apps Monet + Share logs