Skip to content

Commit

Permalink
[feat]: search text in crash log
Browse files Browse the repository at this point in the history
  • Loading branch information
F0x1d committed Aug 18, 2024
1 parent 2968292 commit 47655c3
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 38 deletions.
3 changes: 3 additions & 0 deletions core/ui/src/main/kotlin/com/f0x1d/logfox/ui/Colors.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.f0x1d.logfox.ui

typealias Colors = R.color
3 changes: 2 additions & 1 deletion core/ui/src/main/res/drawable/ic_search.xml
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?colorControlNormal"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="?colorControlNormal"
android:fillColor="#fff"
android:pathData="M380,640Q271,640 195.5,564.5Q120,489 120,380Q120,271 195.5,195.5Q271,120 380,120Q489,120 564.5,195.5Q640,271 640,380Q640,424 626,463Q612,502 588,532L812,756Q823,767 823,784Q823,801 812,812Q801,823 784,823Q767,823 756,812L532,588Q502,612 463,626Q424,640 380,640ZM380,560Q455,560 507.5,507.5Q560,455 560,380Q560,305 507.5,252.5Q455,200 380,200Q305,200 252.5,252.5Q200,305 200,380Q200,455 252.5,507.5Q305,560 380,560Z" />
</vector>
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,17 @@ import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.text.Spannable
import android.text.style.BackgroundColorSpan
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.SearchView
import androidx.core.text.toSpannable
import androidx.core.view.isVisible
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
Expand All @@ -22,6 +29,7 @@ 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.strings.Strings
import com.f0x1d.logfox.ui.Colors
import com.f0x1d.logfox.ui.Icons
import com.f0x1d.logfox.ui.dialog.showAreYouSureDeleteDialog
import com.f0x1d.logfox.ui.dialog.showAreYouSureDialog
Expand All @@ -30,6 +38,7 @@ import com.f0x1d.logfox.ui.view.setClickListenerOn
import com.f0x1d.logfox.ui.view.setupBackButtonForNavController
import dagger.hilt.android.AndroidEntryPoint
import dev.chrisbanes.insetter.applyInsetter
import java.util.Locale

@AndroidEntryPoint
class CrashDetailsFragment: BaseViewModelFragment<CrashDetailsViewModel, FragmentCrashDetailsBinding>() {
Expand All @@ -42,6 +51,12 @@ class CrashDetailsFragment: BaseViewModelFragment<CrashDetailsViewModel, Fragmen
viewModel.exportCrashToZip(it ?: return@registerForActivityResult)
}

private val closeSearchOnBackPressedCallback = object : OnBackPressedCallback(false) {
override fun handleOnBackPressed() {
binding.searchItem.collapseActionView()
}
}

override fun inflateBinding(
inflater: LayoutInflater,
container: ViewGroup?,
Expand All @@ -61,6 +76,29 @@ class CrashDetailsFragment: BaseViewModelFragment<CrashDetailsViewModel, Fragmen
&& viewModel.useSeparateNotificationsChannelsForCrashes
)
}
searchItem.setOnActionExpandListener(
object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
closeSearchOnBackPressedCallback.isEnabled = false
return true
}

override fun onMenuItemActionExpand(item: MenuItem): Boolean {
closeSearchOnBackPressedCallback.isEnabled = true
return true
}
}
)
(searchItem.actionView as SearchView).setOnQueryTextListener(
object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean = true

override fun onQueryTextChange(newText: String?): Boolean {
searchInLog(newText ?: return false)
return true
}
}
)

viewModel.crash.collectWithLifecycle {
setupFor(it ?: return@collectWithLifecycle)
Expand All @@ -77,6 +115,36 @@ class CrashDetailsFragment: BaseViewModelFragment<CrashDetailsViewModel, Fragmen
}
}
}

requireActivity().onBackPressedDispatcher.apply {
addCallback(viewLifecycleOwner, closeSearchOnBackPressedCallback)
}
}

private fun FragmentCrashDetailsBinding.searchInLog(text: String) {
var stackTrace = viewModel.crash.value?.second ?: return
var query = text

val span = stackTrace.toSpannable()
if (query.isNotEmpty()) {
query = query.lowercase(Locale.ENGLISH)
stackTrace = stackTrace.lowercase(Locale.ENGLISH)

val size = query.length
var index = 0
val highlightColor = requireContext().getColor(Colors.md_theme_primaryContainer)
while (stackTrace.indexOf(query, index).also { index = it } != -1) {
span.setSpan(
BackgroundColorSpan(highlightColor),
index,
index + size,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
index += size
}
}
logText.setText(span, TextView.BufferType.SPANNABLE)
logTextScrollable.setText(span, TextView.BufferType.SPANNABLE)
}

@SuppressLint("InlinedApi")
Expand Down Expand Up @@ -143,4 +211,7 @@ class CrashDetailsFragment: BaseViewModelFragment<CrashDetailsViewModel, Fragmen
logText.text = crashLog
logTextScrollable.text = crashLog
}

private val FragmentCrashDetailsBinding.searchItem get() =
toolbar.menu.findItem(R.id.search_item)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import android.app.Application
import android.content.Context
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.AppCrash
import com.f0x1d.logfox.database.entity.AppCrashesCount
Expand All @@ -18,20 +17,16 @@ 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.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
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 kotlinx.coroutines.withContext
import javax.inject.Inject

@HiltViewModel
Expand All @@ -40,7 +35,6 @@ class CrashesViewModel @Inject constructor(
private val disabledAppsRepository: DisabledAppsRepository,
private val appPreferences: AppPreferences,
@DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher,
@IODispatcher private val ioDispatcher: CoroutineDispatcher,
application: Application,
): BaseViewModel(application), AppsPickerResultHandler {

Expand Down Expand Up @@ -84,38 +78,23 @@ class CrashesViewModel @Inject constructor(

val query = MutableStateFlow("")

val searchedCrashes = channelFlow {
combine(crashesRepository.getAllAsFlow(), query) { crashes, query ->
crashes to query
}.collectLatest { (crashes, query) ->

fun AppCrash.suits(query: String): Boolean =
packageName.contains(query, ignoreCase = true)
|| appName?.contains(query, ignoreCase = true) == true

val filteredCrashes = withContext(defaultDispatcher) {
crashes.filter { it.suits(query) }.map { AppCrashesCount(it) }
}

send(filteredCrashes)
delay(SEARCH_DEBOUNCE_MILLIS) // Maybe no need to search content for now

val deeplyFilteredCrashes = withContext(defaultDispatcher) {
crashes.filter { crash ->
val fileContentSettles = withContext(ioDispatcher) {
crash.logFile?.readText()?.contains(query, ignoreCase = true) == true
}

crash.suits(query) || fileContentSettles
}.map { AppCrashesCount(it) }
}
send(deeplyFilteredCrashes)
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) }
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = emptyList(),
)
.distinctUntilChanged()
.flowOn(defaultDispatcher)
.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = emptyList(),
)

fun updateQuery(query: String) = this.query.update { query }

Expand Down
7 changes: 7 additions & 0 deletions feature/crashes/src/main/res/menu/crash_details_menu.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">

<item
android:id="@+id/search_item"
android:title="@string/search"
android:icon="@drawable/ic_search"
app:showAsAction="collapseActionView|ifRoom"
app:actionViewClass="androidx.appcompat.widget.SearchView" />

<item
android:id="@+id/info_item"
android:icon="@drawable/ic_info"
Expand Down

0 comments on commit 47655c3

Please sign in to comment.