diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..d340403c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,43 @@ +on: + workflow_dispatch: + + push: + branches: + - 'master' + +env: + KEY_STORE_PASSWORD: ${{ secrets.KEY_STORE_PASSWORD }} + ALIAS: ${{ secrets.ALIAS }} + KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} + +jobs: + build: + name: Build APK + runs-on: macos-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup JDK + uses: actions/setup-java@v4 + with: + java-version: 17 + distribution: oracle + cache: 'gradle' + + - name: Checkout keystore repository + uses: actions/checkout@v4 + with: + repository: ${{ secrets.KEYSTORE_GIT_REPOSITORY }} + token: ${{ secrets.KEYSTORE_ACCESS_TOKEN }} + path: app/keystore + + - name: Build APK + run: bash ./gradlew assembleRelease --stacktrace --no-daemon + + - name: Upload APK + uses: actions/upload-artifact@v4 + with: + name: logfox-release + compression-level: 0 + path: app/build/outputs/apk/release/app-release.apk diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 16c6fad3..7a24e863 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -9,8 +9,8 @@ android { defaultConfig { applicationId = logFoxPackageName - versionCode = 60 - versionName = "2.0.0" + versionCode = 61 + versionName = "2.0.1" } } diff --git a/app/src/main/kotlin/com/f0x1d/logfox/ui/activity/MainActivity.kt b/app/src/main/kotlin/com/f0x1d/logfox/ui/activity/MainActivity.kt index 995aa339..35c8f993 100644 --- a/app/src/main/kotlin/com/f0x1d/logfox/ui/activity/MainActivity.kt +++ b/app/src/main/kotlin/com/f0x1d/logfox/ui/activity/MainActivity.kt @@ -9,6 +9,9 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.constraintlayout.widget.ConstraintSet import androidx.core.os.bundleOf +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding import androidx.navigation.NavController import androidx.navigation.NavDestination import androidx.navigation.fragment.NavHostFragment @@ -70,6 +73,7 @@ class MainActivity: BaseViewModelActivity(), barView?.setOnItemReselectedListener { // Just do nothing } + setupBarInsets() navController.addOnDestinationChangedListener(this@MainActivity) @@ -87,6 +91,10 @@ class MainActivity: BaseViewModelActivity(), viewModel.askedNotificationsPermission = true } + + if (savedInstanceState == null && viewModel.openCrashesOnStartup) { + navController.navigate(Directions.crashes) + } } override fun onPostCreate(savedInstanceState: Bundle?) { @@ -169,6 +177,38 @@ class MainActivity: BaseViewModelActivity(), } } + private fun ActivityMainBinding.setupBarInsets() { + barView?.let { view -> + ViewCompat.setOnApplyWindowInsetsListener(view) { _, insets -> + if (isHorizontalOrientation) { + val statusBarInsets = insets.getInsets(WindowInsetsCompat.Type.statusBars()) + + view.updatePadding( + left = statusBarInsets.left, + top = statusBarInsets.top, + right = statusBarInsets.right, + bottom = statusBarInsets.bottom, + ) + } else { + val navigationBarsInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars()) + val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime()) + + // I don't want to see it above keyboard + val imeBottomInset = (imeInsets.bottom - view.height).coerceAtLeast(0) + + view.updatePadding( + left = navigationBarsInsets.left, + top = navigationBarsInsets.top, + right = navigationBarsInsets.right, + bottom = navigationBarsInsets.bottom + imeBottomInset, + ) + } + + insets + } + } + } + override fun onDestroy() { super.onDestroy() navController.removeOnDestinationChangedListener(this) diff --git a/app/src/main/kotlin/com/f0x1d/logfox/viewmodel/MainViewModel.kt b/app/src/main/kotlin/com/f0x1d/logfox/viewmodel/MainViewModel.kt index 414195ce..8b82e6f4 100644 --- a/app/src/main/kotlin/com/f0x1d/logfox/viewmodel/MainViewModel.kt +++ b/app/src/main/kotlin/com/f0x1d/logfox/viewmodel/MainViewModel.kt @@ -24,6 +24,8 @@ class MainViewModel @Inject constructor( get() = appPreferences.askedNotificationsPermission set(value) { appPreferences.askedNotificationsPermission = value } + val openCrashesOnStartup get() = appPreferences.openCrashesOnStartup + init { load() } diff --git a/build-logic/convention/src/main/kotlin/main/AndroidApplicationConventionPlugin.kt b/build-logic/convention/src/main/kotlin/main/AndroidApplicationConventionPlugin.kt index c9b32f96..3ce6598a 100644 --- a/build-logic/convention/src/main/kotlin/main/AndroidApplicationConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/main/AndroidApplicationConventionPlugin.kt @@ -65,6 +65,12 @@ class AndroidApplicationConventionPlugin : Plugin { } } } + + dependenciesInfo { + // https://gitlab.com/IzzyOnDroid/repo/-/issues/569#note_1997934495 + includeInApk = false + includeInBundle = false + } } dependencies { coreDependencies() } diff --git a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/API.kt b/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/API.kt index 982d04b9..0f55769c 100644 --- a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/API.kt +++ b/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/API.kt @@ -13,7 +13,6 @@ import androidx.annotation.ChecksSdkIntAtLeast @get:ChecksSdkIntAtLeast(api = Q) val gesturesAvailable = SDK_INT >= Q @get:ChecksSdkIntAtLeast(api = O) val contrastedNavBarAvailable = SDK_INT >= O -@get:ChecksSdkIntAtLeast(api = S) val notificationsDynamicColorAvailable = SDK_INT >= S @get:ChecksSdkIntAtLeast(api = O) val notificationsChannelsAvailable = SDK_INT >= O @get:ChecksSdkIntAtLeast(api = TIRAMISU) val shouldRequestNotificationsPermission = SDK_INT >= TIRAMISU @get:ChecksSdkIntAtLeast(api = O) val startForegroundServiceAvailable = notificationsChannelsAvailable diff --git a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/compose/BaseComposeFragment.kt b/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/compose/BaseComposeFragment.kt index a77a2e0f..dfd5e9d5 100644 --- a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/compose/BaseComposeFragment.kt +++ b/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/compose/BaseComposeFragment.kt @@ -4,7 +4,9 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.ViewCompositionStrategy import com.f0x1d.logfox.arch.databinding.FragmentComposeBinding import com.f0x1d.logfox.arch.ui.fragment.BaseFragment @@ -18,8 +20,13 @@ abstract class BaseComposeFragment : BaseFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - binding.composeView.setContent { - Content() + binding.composeView.apply { + consumeWindowInsets = false + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + + setContent { + this@BaseComposeFragment.Content() + } } } diff --git a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/compose/BaseComposeViewModelFragment.kt b/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/compose/BaseComposeViewModelFragment.kt index 2d0d31cc..28b38bee 100644 --- a/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/compose/BaseComposeViewModelFragment.kt +++ b/core/core-arch/src/main/kotlin/com/f0x1d/logfox/arch/ui/fragment/compose/BaseComposeViewModelFragment.kt @@ -6,6 +6,7 @@ import android.view.View import android.view.ViewGroup import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.ViewCompositionStrategy import com.f0x1d.logfox.arch.databinding.FragmentComposeBinding import com.f0x1d.logfox.arch.ui.fragment.BaseViewModelFragment import com.f0x1d.logfox.arch.ui.snackbar @@ -25,6 +26,7 @@ abstract class BaseComposeViewModelFragment : BaseViewModelFr binding.composeView.apply { consumeWindowInsets = false + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { this@BaseComposeViewModelFragment.Content() diff --git a/core/core-database/src/main/kotlin/com/f0x1d/logfox/database/entity/AppCrash.kt b/core/core-database/src/main/kotlin/com/f0x1d/logfox/database/entity/AppCrash.kt index b56f4aa9..0b44c4f3 100644 --- a/core/core-database/src/main/kotlin/com/f0x1d/logfox/database/entity/AppCrash.kt +++ b/core/core-database/src/main/kotlin/com/f0x1d/logfox/database/entity/AppCrash.kt @@ -123,3 +123,8 @@ class FileConverter { @TypeConverter fun fromFile(value: File) = value.absolutePath } + +data class AppCrashesCount( + val lastCrash: AppCrash, + val count: Int = 1, +) diff --git a/core/core-preferences/src/main/kotlin/com/f0x1d/logfox/preferences/shared/AppPreferences.kt b/core/core-preferences/src/main/kotlin/com/f0x1d/logfox/preferences/shared/AppPreferences.kt index 017eb095..23fef81e 100644 --- a/core/core-preferences/src/main/kotlin/com/f0x1d/logfox/preferences/shared/AppPreferences.kt +++ b/core/core-preferences/src/main/kotlin/com/f0x1d/logfox/preferences/shared/AppPreferences.kt @@ -8,6 +8,7 @@ import com.f0x1d.logfox.database.entity.CrashType import com.f0x1d.logfox.model.logline.LogLine import com.f0x1d.logfox.model.preferences.ShowLogValues import com.f0x1d.logfox.preferences.shared.base.BasePreferences +import com.f0x1d.logfox.preferences.shared.crashes.CrashesSort import dagger.hilt.android.qualifiers.ApplicationContext import java.util.Date import javax.inject.Inject @@ -50,6 +51,10 @@ class AppPreferences @Inject constructor( set(value) { put("pref_time_format", value) } val timeFormatFlow get() = flowSharedPreferences.getString("pref_time_format", TIME_FORMAT_DEFAULT).asFlow() + var openCrashesOnStartup + get() = get("pref_open_crashes_page_on_startup", false) + set(value) { put("pref_open_crashes_page_on_startup", value) } + var logsUpdateInterval get() = get("pref_logs_update_interval", LOGS_UPDATE_INTERVAL_DEFAULT) set(value) { put("pref_logs_update_interval", value) } @@ -151,12 +156,30 @@ class AppPreferences @Inject constructor( get() = get("pref_include_device_info_in_archives", true) set(value) { put("pref_include_device_info_in_archives", value) } + var useSeparateNotificationsChannelsForCrashes + get() = get("pref_notifications_use_separate_channels", true) + set(value) { put("pref_notifications_use_separate_channels", value) } + var askedNotificationsPermission get() = get("pref_asked_notifications_permission", false) set(value) { put("pref_asked_notifications_permission", value) } val showLogValues get() = cachedShowLogValues ?: updateCachedShowLogsValues() + val crashesSortType get() = flowSharedPreferences.getEnum( + key = "pref_crashes_sort_type", + defaultValue = CrashesSort.NEW, + ) + val crashesSortReversedOrder get() = flowSharedPreferences.getBoolean( + key = "pref_crashes_sort_reversed_order", + defaultValue = true, + ) + + fun updateCrashesSortSettings(sortType: CrashesSort, sortInReversedOrder: Boolean) { + put("pref_crashes_sort_type", sortType.name) + put("pref_crashes_sort_reversed_order", sortInReversedOrder) + } + fun collectingFor(crashType: CrashType) = get( key = "pref_collect_${crashType.readableName.lowercase()}", defaultValue = true diff --git a/core/core-preferences/src/main/kotlin/com/f0x1d/logfox/preferences/shared/crashes/CrashesSort.kt b/core/core-preferences/src/main/kotlin/com/f0x1d/logfox/preferences/shared/crashes/CrashesSort.kt new file mode 100644 index 00000000..572eea77 --- /dev/null +++ b/core/core-preferences/src/main/kotlin/com/f0x1d/logfox/preferences/shared/crashes/CrashesSort.kt @@ -0,0 +1,31 @@ +package com.f0x1d.logfox.preferences.shared.crashes + +import androidx.annotation.Keep +import androidx.annotation.StringRes +import com.f0x1d.logfox.database.entity.AppCrashesCount +import com.f0x1d.logfox.strings.Strings + +@Keep +enum class CrashesSort( + @StringRes val titleRes: Int, + val sorter: (List) -> List = { it }, +) { + NAME( + titleRes = Strings.sort_by_name, + sorter = { crashes -> + crashes.sortedBy { it.lastCrash.appName ?: it.lastCrash.packageName } + }, + ), + NEW( + titleRes = Strings.sort_by_new, + sorter = { crashes -> + crashes.sortedBy { it.lastCrash.dateAndTime } + }, + ), + COUNT( + titleRes = Strings.sort_by_count, + sorter = { crashes -> + crashes.sortedBy { it.count } + }, + ), +} diff --git a/core/core-ui/src/main/res/drawable/ic_sort.xml b/core/core-ui/src/main/res/drawable/ic_sort.xml new file mode 100644 index 00000000..84fbb970 --- /dev/null +++ b/core/core-ui/src/main/res/drawable/ic_sort.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/feature/feature-crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/controller/CrashesNotificationsController.kt b/feature/feature-crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/controller/CrashesNotificationsController.kt index 19ca8b85..f7d58469 100644 --- a/feature/feature-crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/controller/CrashesNotificationsController.kt +++ b/feature/feature-crashes-core/src/main/kotlin/com/f0x1d/logfox/feature/crashes/core/controller/CrashesNotificationsController.kt @@ -19,6 +19,7 @@ import com.f0x1d.logfox.intents.COPY_CRASH_INTENT_ID import com.f0x1d.logfox.intents.makeBroadcastPendingIntent import com.f0x1d.logfox.navigation.Directions import com.f0x1d.logfox.navigation.NavGraphs +import com.f0x1d.logfox.preferences.shared.AppPreferences import com.f0x1d.logfox.strings.Strings import com.f0x1d.logfox.ui.Icons import dagger.hilt.android.qualifiers.ApplicationContext @@ -33,6 +34,7 @@ internal interface CrashesNotificationsController { @SuppressLint("MissingPermission") internal class CrashesNotificationsControllerImpl @Inject constructor( @ApplicationContext private val context: Context, + private val appPreferences: AppPreferences, ) : CrashesNotificationsController { override fun sendErrorNotification(appCrash: AppCrash, crashLog: String?) { @@ -42,7 +44,14 @@ internal class CrashesNotificationsControllerImpl @Inject constructor( notify( appCrash.packageName, appCrash.notificationId, - NotificationCompat.Builder(context, appCrash.notificationChannelId) + NotificationCompat.Builder( + context, + if (appPreferences.useSeparateNotificationsChannelsForCrashes) { + appCrash.notificationChannelId + } else { + CRASHES_CHANNEL_ID + }, + ) .setContentTitle( context.getString( Strings.app_crashed, @@ -80,26 +89,41 @@ internal class CrashesNotificationsControllerImpl @Inject constructor( } private fun createNotificationChannelFor(appCrash: AppCrash) { - val crashTypeName = appCrash.crashType.readableName - val groupId = "${CRASHES_CHANNEL_GROUP_ID}_$crashTypeName" + if (appPreferences.useSeparateNotificationsChannelsForCrashes) { + val crashTypeName = appCrash.crashType.readableName + val groupId = "${CRASHES_CHANNEL_GROUP_ID}_$crashTypeName" - val crashesGroup = NotificationChannelGroupCompat.Builder(groupId) - .setName(context.getString(Strings.type_crashes, crashTypeName)) - .build() + val crashesGroup = NotificationChannelGroupCompat.Builder(groupId) + .setName(context.getString(Strings.type_crashes, crashTypeName)) + .build() - val appCrashesChannel = NotificationChannelCompat.Builder( - appCrash.notificationChannelId, - NotificationManagerCompat.IMPORTANCE_HIGH, - ) - .setName(context.getString(Strings.crashes_of, crashTypeName, appCrash.packageName)) - .setLightsEnabled(true) - .setVibrationEnabled(true) - .setGroup(groupId) - .build() + val appCrashesChannel = NotificationChannelCompat.Builder( + appCrash.notificationChannelId, + NotificationManagerCompat.IMPORTANCE_HIGH, + ) + .setName(context.getString(Strings.crashes_of, crashTypeName, appCrash.packageName)) + .setLightsEnabled(true) + .setVibrationEnabled(true) + .setGroup(groupId) + .build() + + context.notificationManagerCompat.apply { + createNotificationChannelGroupsCompat(listOf(crashesGroup)) + createNotificationChannelsCompat(listOf(appCrashesChannel)) + } + } else { + val crashesChannel = NotificationChannelCompat.Builder( + CRASHES_CHANNEL_ID, + NotificationManagerCompat.IMPORTANCE_HIGH, + ) + .setName(context.getString(Strings.crashes)) + .setLightsEnabled(true) + .setVibrationEnabled(true) + .build() - context.notificationManagerCompat.apply { - createNotificationChannelGroupsCompat(listOf(crashesGroup)) - createNotificationChannelsCompat(listOf(appCrashesChannel)) + context.notificationManagerCompat.apply { + createNotificationChannelsCompat(listOf(crashesChannel)) + } } } @@ -115,6 +139,10 @@ internal class CrashesNotificationsControllerImpl @Inject constructor( if (it.tag != null && it.tag.contains(".")) cancel(it.tag, it.id) } } + + companion object { + private const val CRASHES_CHANNEL_ID = "crashes" + } } val AppCrash.notificationChannelId get() = "${CRASHES_CHANNEL_GROUP_ID}_${crashType.readableName}_$packageName" diff --git a/feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/adapter/CrashesAdapter.kt b/feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/adapter/CrashesAdapter.kt index 934e7961..97c561f3 100644 --- a/feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/adapter/CrashesAdapter.kt +++ b/feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/adapter/CrashesAdapter.kt @@ -4,8 +4,8 @@ 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.AppCrashesCount import com.f0x1d.logfox.feature.crashes.databinding.ItemCrashBinding -import com.f0x1d.logfox.feature.crashes.model.AppCrashesCount import com.f0x1d.logfox.feature.crashes.ui.viewholder.CrashViewHolder class CrashesAdapter( diff --git a/feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/model/AppCrashesCount.kt b/feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/model/AppCrashesCount.kt deleted file mode 100644 index d7f9477a..00000000 --- a/feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/model/AppCrashesCount.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.f0x1d.logfox.feature.crashes.model - -import com.f0x1d.logfox.database.entity.AppCrash - -data class AppCrashesCount( - val lastCrash: AppCrash, - val count: Int = 1, -) diff --git a/feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/ui/fragment/CrashDetailsFragment.kt b/feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/ui/fragment/CrashDetailsFragment.kt index 52b2cb41..91b10b51 100644 --- a/feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/ui/fragment/CrashDetailsFragment.kt +++ b/feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/ui/fragment/CrashDetailsFragment.kt @@ -69,7 +69,10 @@ class CrashDetailsFragment: BaseViewModelFragment + viewModel.updateQuery(text?.toString().orEmpty()) + } + + addTransitionListener { _, _, newState -> + closeSearchOnBackPressedCallback.isEnabled = newState == SearchView.TransitionState.SHOWN } } @@ -86,10 +138,66 @@ class CrashesFragment: BaseViewModelFragment + ItemSortBinding.inflate(layoutInflater).root.apply { + id = View.generateViewId() + text = getString(type.titleRes) + tag = type + + setOnCheckedChangeListener { _, isChecked -> + if (isChecked) { + selectedSortType = type + } + } + } + }.forEach { button -> + dialogBinding.rgSorting.apply { + addView(button) + + if (button.tag == viewModel.currentSort) { + check(button.id) + } + } + } + + dialogBinding.reverseSwitch.apply { + isChecked = sortInReversedOrder + setOnCheckedChangeListener { _, isChecked -> sortInReversedOrder = isChecked } + } + + MaterialAlertDialogBuilder(requireContext()) + .setTitle(Strings.sort) + .setView(dialogBinding.root) + .setPositiveButton(android.R.string.ok) { _, _ -> + viewModel.updateSort( + sortType = selectedSortType, + sortInReversedOrder = sortInReversedOrder, + ) + } + .show() } } diff --git a/feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/ui/viewholder/CrashViewHolder.kt b/feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/ui/viewholder/CrashViewHolder.kt index 57341a03..c1610c0a 100644 --- a/feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/ui/viewholder/CrashViewHolder.kt +++ b/feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/ui/viewholder/CrashViewHolder.kt @@ -3,8 +3,8 @@ package com.f0x1d.logfox.feature.crashes.ui.viewholder import android.annotation.SuppressLint import com.bumptech.glide.Glide import com.f0x1d.logfox.arch.ui.viewholder.BaseViewHolder +import com.f0x1d.logfox.database.entity.AppCrashesCount import com.f0x1d.logfox.feature.crashes.databinding.ItemCrashBinding -import com.f0x1d.logfox.feature.crashes.model.AppCrashesCount import com.f0x1d.logfox.strings.Strings import com.f0x1d.logfox.ui.view.loadIcon import java.util.Date diff --git a/feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/viewmodel/CrashDetailsViewModel.kt b/feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/viewmodel/CrashDetailsViewModel.kt index 1fc88ae5..e6b34801 100644 --- a/feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/viewmodel/CrashDetailsViewModel.kt +++ b/feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/viewmodel/CrashDetailsViewModel.kt @@ -25,7 +25,7 @@ class CrashDetailsViewModel @Inject constructor( @CrashId val crashId: Long, val dateTimeFormatter: DateTimeFormatter, private val crashesRepository: CrashesRepository, - private val appPreferences: AppPreferences, + val appPreferences: AppPreferences, @IODispatcher private val ioDispatcher: CoroutineDispatcher, application: Application, ): BaseViewModel(application) { diff --git a/feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/viewmodel/list/AppCrashesViewModel.kt b/feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/viewmodel/list/AppCrashesViewModel.kt index dca38100..9aaba540 100644 --- a/feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/viewmodel/list/AppCrashesViewModel.kt +++ b/feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/viewmodel/list/AppCrashesViewModel.kt @@ -2,13 +2,13 @@ package com.f0x1d.logfox.feature.crashes.viewmodel.list import android.app.Application import androidx.lifecycle.viewModelScope -import com.f0x1d.logfox.arch.di.IODispatcher +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.core.repository.CrashesRepository import com.f0x1d.logfox.feature.crashes.di.AppName import com.f0x1d.logfox.feature.crashes.di.PackageName -import com.f0x1d.logfox.feature.crashes.model.AppCrashesCount import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.SharingStarted @@ -22,7 +22,7 @@ class AppCrashesViewModel @Inject constructor( @PackageName val packageName: String, @AppName val appName: String?, private val crashesRepository: CrashesRepository, - @IODispatcher private val ioDispatcher: CoroutineDispatcher, + @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher, application: Application, ): BaseViewModel(application) { @@ -34,14 +34,14 @@ class AppCrashesViewModel @Inject constructor( AppCrashesCount(it) } } - .flowOn(ioDispatcher) + .flowOn(defaultDispatcher) .stateIn( scope = viewModelScope, started = SharingStarted.Eagerly, initialValue = emptyList(), ) - fun deleteCrash(appCrash: AppCrash) = launchCatching(ioDispatcher) { + fun deleteCrash(appCrash: AppCrash) = launchCatching { crashesRepository.delete(appCrash) } } diff --git a/feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/viewmodel/list/CrashesViewModel.kt b/feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/viewmodel/list/CrashesViewModel.kt index d655f8e9..6062709c 100644 --- a/feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/viewmodel/list/CrashesViewModel.kt +++ b/feature/feature-crashes/src/main/kotlin/com/f0x1d/logfox/feature/crashes/viewmodel/list/CrashesViewModel.kt @@ -2,51 +2,116 @@ package com.f0x1d.logfox.feature.crashes.viewmodel.list import android.app.Application import androidx.lifecycle.viewModelScope -import com.f0x1d.logfox.arch.di.IODispatcher +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.core.repository.CrashesRepository -import com.f0x1d.logfox.feature.crashes.model.AppCrashesCount +import com.f0x1d.logfox.preferences.shared.AppPreferences +import com.f0x1d.logfox.preferences.shared.crashes.CrashesSort 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.debounce 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 javax.inject.Inject @HiltViewModel class CrashesViewModel @Inject constructor( private val crashesRepository: CrashesRepository, - @IODispatcher private val ioDispatcher: CoroutineDispatcher, + private val appPreferences: AppPreferences, + @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher, application: Application, ): BaseViewModel(application) { - val crashes = crashesRepository.getAllAsFlow() + 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 { crashes -> - val groupedCrashes = crashes.groupBy { it.packageName } + .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(ioDispatcher) + .flowOn(defaultDispatcher) .stateIn( scope = viewModelScope, started = SharingStarted.Eagerly, initialValue = emptyList(), ) - fun deleteCrashesByPackageName(appCrash: AppCrash) = launchCatching(ioDispatcher) { + val query = MutableStateFlow("") + + val searchedCrashes = combine( + crashesRepository.getAllAsFlow(), + query, + ) { crashes, query -> + crashes to query + }.debounce( + timeoutMillis = 100, + ).map { (crashes, query) -> + crashes.filter { crash -> + crash.packageName.contains(query, ignoreCase = true) + || crash.appName?.contains(query, ignoreCase = true) == true + }.map { AppCrashesCount(it) } + }.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 clearCrashes() = launchCatching(ioDispatcher) { + fun deleteCrash(appCrash: AppCrash) = launchCatching { + crashesRepository.delete(appCrash) + } + + fun clearCrashes() = launchCatching { crashesRepository.clear() } + + private data class CrashesWithSort( + val crashes: List, + val sortType: CrashesSort, + val sortInReversedOrder: Boolean, + ) } diff --git a/feature/feature-crashes/src/main/res/layout/dialog_sorting.xml b/feature/feature-crashes/src/main/res/layout/dialog_sorting.xml new file mode 100644 index 00000000..6bf593d2 --- /dev/null +++ b/feature/feature-crashes/src/main/res/layout/dialog_sorting.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + diff --git a/feature/feature-crashes/src/main/res/layout/fragment_crashes.xml b/feature/feature-crashes/src/main/res/layout/fragment_crashes.xml index 5ec7327b..6bfb1bc8 100644 --- a/feature/feature-crashes/src/main/res/layout/fragment_crashes.xml +++ b/feature/feature-crashes/src/main/res/layout/fragment_crashes.xml @@ -12,11 +12,15 @@ android:fitsSystemWindows="true" app:liftOnScroll="true"> - + app:navigationIcon="@drawable/ic_search" + app:navigationIconTint="?colorControlNormal" + app:layout_scrollFlags="enterAlways" + android:hint="@string/crashes"/> + + + + + diff --git a/feature/feature-crashes/src/main/res/layout/item_sort.xml b/feature/feature-crashes/src/main/res/layout/item_sort.xml new file mode 100644 index 00000000..73a0a269 --- /dev/null +++ b/feature/feature-crashes/src/main/res/layout/item_sort.xml @@ -0,0 +1,7 @@ + + diff --git a/feature/feature-crashes/src/main/res/menu/crashes_menu.xml b/feature/feature-crashes/src/main/res/menu/crashes_menu.xml index 3c66443d..58c6d6a1 100644 --- a/feature/feature-crashes/src/main/res/menu/crashes_menu.xml +++ b/feature/feature-crashes/src/main/res/menu/crashes_menu.xml @@ -2,10 +2,16 @@ + + - \ No newline at end of file + diff --git a/feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/ui/fragment/ChooseAppFragment.kt b/feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/ui/fragment/ChooseAppFragment.kt index 8258d869..19340ec6 100644 --- a/feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/ui/fragment/ChooseAppFragment.kt +++ b/feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/ui/fragment/ChooseAppFragment.kt @@ -25,7 +25,6 @@ import com.google.android.material.divider.MaterialDividerItemDecoration import com.google.android.material.search.SearchView import dagger.hilt.android.AndroidEntryPoint import dev.chrisbanes.insetter.applyInsetter -import kotlinx.coroutines.flow.update @AndroidEntryPoint class ChooseAppFragment: BaseViewModelFragment() { @@ -66,13 +65,15 @@ class ChooseAppFragment: BaseViewModelFragment - viewModel.query.update { - editable.toString() + + searchView.apply { + editText.doAfterTextChanged { text -> + viewModel.updateQuery(text?.toString().orEmpty()) + } + + addTransitionListener { _, _, newState -> + closeSearchOnBackPressedCallback.isEnabled = newState == SearchView.TransitionState.SHOWN } - } - searchView.addTransitionListener { _, _, newState -> - closeSearchOnBackPressedCallback.isEnabled = newState == SearchView.TransitionState.SHOWN } listOf(appsRecycler, searchedAppsRecycler).forEach { diff --git a/feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/viewmodel/ChooseAppViewModel.kt b/feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/viewmodel/ChooseAppViewModel.kt index 7f5ed5c5..8799af2e 100644 --- a/feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/viewmodel/ChooseAppViewModel.kt +++ b/feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/viewmodel/ChooseAppViewModel.kt @@ -2,7 +2,7 @@ package com.f0x1d.logfox.feature.filters.viewmodel import android.app.Application import androidx.lifecycle.viewModelScope -import com.f0x1d.logfox.arch.di.IODispatcher +import com.f0x1d.logfox.arch.di.DefaultDispatcher import com.f0x1d.logfox.arch.viewmodel.BaseViewModel import com.f0x1d.logfox.model.InstalledApp import dagger.hilt.android.lifecycle.HiltViewModel @@ -10,7 +10,6 @@ import kotlinx.coroutines.CoroutineDispatcher 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 @@ -19,7 +18,7 @@ import javax.inject.Inject @HiltViewModel class ChooseAppViewModel @Inject constructor( - @IODispatcher private val ioDispatcher: CoroutineDispatcher, + @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher, application: Application, ): BaseViewModel(application) { @@ -32,19 +31,21 @@ class ChooseAppViewModel @Inject constructor( it.first.filter { app -> app.title.toString().contains(it.second) || app.packageName.contains(it.second) } - }.flowOn(ioDispatcher) - .distinctUntilChanged() - .stateIn( - scope = viewModelScope, - started = SharingStarted.Eagerly, - initialValue = emptyList(), - ) + }.flowOn( + defaultDispatcher, + ).stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = emptyList(), + ) init { load() } - private fun load() = launchCatching(ioDispatcher) { + fun updateQuery(text: String) = query.update { text } + + private fun load() = launchCatching(defaultDispatcher) { val packageManager = ctx.packageManager val installedApps = packageManager.getInstalledPackages(0).map { diff --git a/feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/viewmodel/EditFilterViewModel.kt b/feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/viewmodel/EditFilterViewModel.kt index a3baaf78..9246ebb6 100644 --- a/feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/viewmodel/EditFilterViewModel.kt +++ b/feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/viewmodel/EditFilterViewModel.kt @@ -8,6 +8,7 @@ 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.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 @@ -19,7 +20,6 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel @@ -70,7 +70,7 @@ class EditFilterViewModel @Inject constructor( val tag = MutableStateFlow(null) val content = MutableStateFlow(null) - fun create() = viewModelScope.launch { + fun create() = launchCatching { filtersRepository.create( including.value, enabledLogLevels.toEnabledLogLevels(), @@ -78,7 +78,7 @@ class EditFilterViewModel @Inject constructor( ) } - fun update(userFilter: UserFilter) = viewModelScope.launch { + fun update(userFilter: UserFilter) = launchCatching { filtersRepository.update( userFilter, including.value, @@ -99,7 +99,7 @@ class EditFilterViewModel @Inject constructor( enabledLogLevels[which] = filtering } - fun selectApp(app: com.f0x1d.logfox.model.InstalledApp) = packageName.update { + fun selectApp(app: InstalledApp) = packageName.update { app.packageName }.also { sendEvent(EVENT_TYPE_UPDATE_PACKAGE_NAME_TEXT) diff --git a/feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/viewmodel/FiltersViewModel.kt b/feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/viewmodel/FiltersViewModel.kt index ca96540a..91c1212b 100644 --- a/feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/viewmodel/FiltersViewModel.kt +++ b/feature/feature-filters/src/main/kotlin/com/f0x1d/logfox/feature/filters/viewmodel/FiltersViewModel.kt @@ -14,7 +14,6 @@ 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 @@ -52,15 +51,15 @@ class FiltersViewModel @Inject constructor( } } - fun switch(userFilter: UserFilter, checked: Boolean) = viewModelScope.launch { + fun switch(userFilter: UserFilter, checked: Boolean) = launchCatching { filtersRepository.switch(userFilter, checked) } - fun delete(userFilter: UserFilter) = viewModelScope.launch { + fun delete(userFilter: UserFilter) = launchCatching { filtersRepository.delete(userFilter) } - fun clearAll() = viewModelScope.launch { + fun clearAll() = launchCatching { filtersRepository.clear() } } diff --git a/feature/feature-logging/src/main/kotlin/com/f0x1d/feature/logging/service/LoggingService.kt b/feature/feature-logging/src/main/kotlin/com/f0x1d/feature/logging/service/LoggingService.kt index 7fdf6526..37f3a461 100644 --- a/feature/feature-logging/src/main/kotlin/com/f0x1d/feature/logging/service/LoggingService.kt +++ b/feature/feature-logging/src/main/kotlin/com/f0x1d/feature/logging/service/LoggingService.kt @@ -88,7 +88,6 @@ class LoggingService : LifecycleService() { private val logs = LinkedList() private val logsMutex = Mutex() private var loggingJob: Job? = null - private var idsCounter = -1L private lateinit var filtersState: StateFlow> @@ -140,7 +139,7 @@ class LoggingService : LifecycleService() { while (true) { loggingRepository.startLogging( terminal = loggingTerminal, - startingId = idsCounter, + startingId = logs.lastOrNull()?.id ?: 0, ).catch { throwable -> if (throwable is TerminalNotSupportedException) { if (appPreferences.fallbackToDefaultTerminal) { diff --git a/feature/feature-logging/src/main/kotlin/com/f0x1d/feature/logging/ui/fragment/LogsFragment.kt b/feature/feature-logging/src/main/kotlin/com/f0x1d/feature/logging/ui/fragment/LogsFragment.kt index 04f1aa21..c38a9868 100644 --- a/feature/feature-logging/src/main/kotlin/com/f0x1d/feature/logging/ui/fragment/LogsFragment.kt +++ b/feature/feature-logging/src/main/kotlin/com/f0x1d/feature/logging/ui/fragment/LogsFragment.kt @@ -125,7 +125,6 @@ class LogsFragment: BaseViewModelFragment() } setClickListenerOn(R.id.clear_item) { requireContext().sendService(action = LoggingService.ACTION_CLEAR_LOGS) - updateLogsList(null) } setClickListenerOn(R.id.restart_logging_item) { requireContext().sendService(action = LoggingService.ACTION_RESTART_LOGGING) diff --git a/feature/feature-logging/src/main/kotlin/com/f0x1d/feature/logging/viewmodel/LogsViewModel.kt b/feature/feature-logging/src/main/kotlin/com/f0x1d/feature/logging/viewmodel/LogsViewModel.kt index 3584fd17..edae13d2 100644 --- a/feature/feature-logging/src/main/kotlin/com/f0x1d/feature/logging/viewmodel/LogsViewModel.kt +++ b/feature/feature-logging/src/main/kotlin/com/f0x1d/feature/logging/viewmodel/LogsViewModel.kt @@ -27,7 +27,6 @@ import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.scan import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject @@ -74,24 +73,32 @@ class LogsViewModel @Inject constructor( if (!viewingFile) paused else flowOf(false) ) { logs, filters, query, paused -> LogsData(logs, filters, query, paused) - }.scan(null as LogsData?) { accumulator, data -> + }.scan(LogsData()) { accumulator, data -> when { - !data.paused -> data - - data.query != accumulator?.query || data.filters != accumulator?.filters -> data.copy( - logs = accumulator?.logs ?: emptyList() + !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 { - it?.passing == true - }.mapNotNull { - it?.run { - logs.filterAndSearch(filters, query) + else -> data.copy( + logs = accumulator.logs, + passing = false, + ) } + }.filter { data -> + data.passing + }.mapNotNull { data -> + data.logs.filterAndSearch( + filters = data.filters, + query = data.query, + ) }.flowOn( - ioDispatcher, + defaultDispatcher, ).stateIn( scope = viewModelScope, started = SharingStarted.Eagerly, @@ -109,16 +116,14 @@ class LogsViewModel @Inject constructor( } fun selectAll() { - if (selectedItems.value == logs.value) selectedItems.update { + if (selectedItems.value.containsAll(logs.value)) selectedItems.update { emptySet() } else selectedItems.update { logs.value.toSet() } } - fun clearSelection() = selectedItems.update { emptySet() } - - fun selectedToRecording() = viewModelScope.launch { + fun selectedToRecording() = launchCatching { recordingsRepository.createRecordingFrom( lines = withContext(defaultDispatcher) { selectedItems.value.sortedBy { it.dateAndTime } @@ -155,10 +160,10 @@ class LogsViewModel @Inject constructor( } private data class LogsData( - val logs: List, - val filters: List, - val query: String?, - val paused: Boolean, + val logs: List = emptyList(), + val filters: List = emptyList(), + val query: String? = null, + val paused: Boolean = false, val passing: Boolean = true, ) } diff --git a/feature/feature-recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/viewmodel/RecordingViewModel.kt b/feature/feature-recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/viewmodel/RecordingViewModel.kt index 36ca14c7..17e9cdad 100644 --- a/feature/feature-recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/viewmodel/RecordingViewModel.kt +++ b/feature/feature-recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/viewmodel/RecordingViewModel.kt @@ -17,11 +17,9 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import javax.inject.Inject @@ -38,7 +36,6 @@ class RecordingViewModel @Inject constructor( val recording = recordingsRepository.getByIdAsFlow(recordingId) .distinctUntilChanged() - .flowOn(ioDispatcher) .onEach { recording -> currentTitle.update { recording?.title } } @@ -80,7 +77,7 @@ class RecordingViewModel @Inject constructor( } } - fun updateTitle(title: String) = viewModelScope.launch { + fun updateTitle(title: String) = launchCatching { titleUpdateMutex.withLock { recording.value?.let { recordingsRepository.updateTitle(it, title) diff --git a/feature/feature-recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/viewmodel/RecordingsViewModel.kt b/feature/feature-recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/viewmodel/RecordingsViewModel.kt index 30a010a7..47c84fa1 100644 --- a/feature/feature-recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/viewmodel/RecordingsViewModel.kt +++ b/feature/feature-recordings/src/main/kotlin/com/f0x1d/logfox/feature/recordings/viewmodel/RecordingsViewModel.kt @@ -1,8 +1,6 @@ package com.f0x1d.logfox.feature.recordings.viewmodel import android.app.Application -import androidx.lifecycle.viewModelScope -import com.f0x1d.logfox.arch.di.IODispatcher import com.f0x1d.logfox.arch.viewmodel.BaseViewModel import com.f0x1d.logfox.database.entity.LogRecording import com.f0x1d.logfox.feature.recordings.core.controller.RecordingController @@ -10,17 +8,13 @@ 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.CoroutineDispatcher import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class RecordingsViewModel @Inject constructor( private val recordingsRepository: RecordingsRepository, private val recordingController: RecordingController, - @IODispatcher private val ioDispatcher: CoroutineDispatcher, application: Application, ): BaseViewModel(application) { @@ -30,11 +24,10 @@ class RecordingsViewModel @Inject constructor( val recordings = recordingsRepository.getAllAsFlow() .distinctUntilChanged() - .flowOn(ioDispatcher) val recordingState = recordingController.recordingState - fun toggleStartStop() = viewModelScope.launch { + fun toggleStartStop() = launchCatching { if (recordingState.value == RecordingState.IDLE) recordingController.record() else @@ -43,25 +36,25 @@ class RecordingsViewModel @Inject constructor( } } - fun togglePauseResume() = viewModelScope.launch { + fun togglePauseResume() = launchCatching { if (recordingState.value == RecordingState.PAUSED) recordingController.resume() else recordingController.pause() } - fun clearRecordings() = viewModelScope.launch { + fun clearRecordings() = launchCatching { recordingsRepository.clear() } - fun saveAll() = viewModelScope.launch { + fun saveAll() = launchCatching { snackbar(Strings.saving_logs) recordingsRepository.saveAll().also { sendEvent(EVENT_TYPE_RECORDING_SAVED, it) } } - fun delete(logRecording: LogRecording) = viewModelScope.launch { + fun delete(logRecording: LogRecording) = launchCatching { recordingsRepository.delete(logRecording) } } diff --git a/feature/feature-settings/src/main/res/xml/settings_notifications.xml b/feature/feature-settings/src/main/res/xml/settings_notifications.xml index c730ff5c..301d054a 100644 --- a/feature/feature-settings/src/main/res/xml/settings_notifications.xml +++ b/feature/feature-settings/src/main/res/xml/settings_notifications.xml @@ -32,6 +32,14 @@ android:selectable="false" app:iconSpaceReserved="false" /> + + + + Unit, val onAdbClick: () -> Unit, diff --git a/strings/src/main/res/values-ru/strings.xml b/strings/src/main/res/values-ru/strings.xml index eb99c465..d6ec9080 100644 --- a/strings/src/main/res/values-ru/strings.xml +++ b/strings/src/main/res/values-ru/strings.xml @@ -146,4 +146,11 @@ %s сбои %s сбои %s Настройки персонализированных уведомлений для приложений доступны в деталях сбоя и системных настройках LogFox + Сортировка + В обратном порядке + По имени + По новизне + По количеству сбоев + Использовать разные каналы уведомлений для уведомлений о сбоях + Открывать вкладку сбоев при запуске diff --git a/strings/src/main/res/values/strings.xml b/strings/src/main/res/values/strings.xml index 4316e53b..860244b1 100644 --- a/strings/src/main/res/values/strings.xml +++ b/strings/src/main/res/values/strings.xml @@ -155,4 +155,11 @@ %s crashes %s crashes of %s Per app notifications settings are available in app\'s crash details and LogFox\'s system settings + Sort + In reversed order + By name + By novelty + By crashes count + Use separate notifications channels for crashes notifications + Open crashes page on startup