Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ paging-common = "3.1.1"
paging-runtime = "3.1.1"
preference = "1.2.0"
recyclerview = "1.3.0"
robolectric = "4.16"
room = "2.7.2"
room-common = "2.7.2"
rx-android = "3.0.2"
Expand Down Expand Up @@ -78,6 +79,7 @@ androidx-paging-runtime-ktx = { group = "androidx.paging", name = "paging-runtim
androidx-preference-ktx = { group = "androidx.preference", name = "preference-ktx", version.ref = "preference" }
androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" }
androidx-room-common = { module = "androidx.room:room-common", version.ref = "room-common" }
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
Expand Down
5 changes: 4 additions & 1 deletion libraries/analytics-logger/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ android {
}

group = "com.trendyol.android.devtools"
version = "0.8.0"
version = "0.9.0"

publishConfig {
defaultConfiguration(
Expand All @@ -59,4 +59,7 @@ dependencies {
ksp(libs.androidx.room.compiler)
implementation(libs.embeddedKoinCore)
implementation(libs.embeddedKoinAndroid)

testImplementation(libs.junit)
testImplementation(libs.robolectric)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package com.trendyol.android.devtools.analyticslogger.internal.ext

import android.graphics.Typeface
import android.text.SpannableString
import android.text.style.BackgroundColorSpan
import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan

/**
* Highlights all occurrences of the query string in the text with background and foreground colors
* Safe implementation that handles edge cases and prevents crashes
*/
internal fun String.highlightQuery(query: String?, highlightBackgroundColor: Int, defaultTextColor: Int): SpannableString {
val spannable = SpannableString(this)

if (query.isNullOrEmpty() || query.length < 2) {
return spannable
}

try {
var startIndex = 0
while (startIndex < this.length) {
val index = this.indexOf(query, startIndex, ignoreCase = true)
if (index == -1) break

// Safety check: ensure end index doesn't exceed text length
val endIndex = (index + query.length).coerceAtMost(this.length)

// Only apply span if we have a valid range
if (index >= 0 && endIndex <= this.length && index < endIndex) {
spannable.setSpan(
BackgroundColorSpan(highlightBackgroundColor),
index,
endIndex,
SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE
)
spannable.setSpan(
ForegroundColorSpan(defaultTextColor),
index,
endIndex,
SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE
)
spannable.setSpan(
StyleSpan(Typeface.BOLD),
index,
endIndex,
SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE
)
}

startIndex = index + query.length
}
} catch (e: Exception) {
// If anything goes wrong, just return the non-highlighted text
android.util.Log.w("TextExtensions", "Error highlighting query: ${e.message}")
return spannable
}

return spannable
}

/**
* Finds the first line in the text that contains the query and returns a preview with context
*/
internal fun String?.findMatchingLinePreview(query: String?): String? {
if (this.isNullOrEmpty() || query.isNullOrEmpty() || query.length < 2) {
return null
}

// Split by common JSON separators to get individual lines/fields
val lines = this.split("\n", ",", "{", "}", "[", "]")
.map { it.trim() }
.filter { it.isNotEmpty() }

// Find the first line that contains the query
val matchingLine = lines.firstOrNull {
it.contains(query, ignoreCase = true)
} ?: return null

// Create a preview with ellipsis if needed
val maxPreviewLength = 100
return if (matchingLine.length > maxPreviewLength) {
val queryIndex = matchingLine.indexOf(query, ignoreCase = true)

// Try to center the query in the preview
val start = (queryIndex - 30).coerceAtLeast(0)
val end = (start + maxPreviewLength).coerceAtMost(matchingLine.length)

val preview = matchingLine.substring(start, end)
val prefix = if (start > 0) "..." else ""
val suffix = if (end < matchingLine.length) "..." else ""

"$prefix$preview$suffix"
} else {
matchingLine
}
}

/**
* Checks if the text contains the query string (case insensitive)
*/
internal fun String?.containsQuery(query: String?): Boolean {
if (this.isNullOrEmpty() || query.isNullOrEmpty()) {
return false
}
return this.contains(query, ignoreCase = true)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package com.trendyol.android.devtools.analyticslogger.internal.ext

import android.annotation.SuppressLint
import android.content.Context
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.widget.SearchView

/**
* Hides the soft keyboard from the current view
*/
internal fun View.hideKeyboard() {
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
imm?.hideSoftInputFromWindow(windowToken, 0)
}

/**
* Sets up RecyclerView to hide keyboard when scrolling or touching
*/
@SuppressLint("ClickableViewAccessibility")
internal fun androidx.recyclerview.widget.RecyclerView.setupHideKeyboardOnScroll() {
addOnScrollListener(object : androidx.recyclerview.widget.RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: androidx.recyclerview.widget.RecyclerView, newState: Int) {
if (newState == androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_DRAGGING) {
hideKeyboard()
}
}
})

// Also hide on touch to handle taps on items
setOnTouchListener { v, event ->
if (event.action == MotionEvent.ACTION_DOWN) {
v.hideKeyboard()
}
false // Don't consume the event
}
}

/**
* Sets up NestedScrollView to hide keyboard when scrolling or touching
*/
@SuppressLint("ClickableViewAccessibility")
internal fun androidx.core.widget.NestedScrollView.setupHideKeyboardOnScroll() {
setOnScrollChangeListener { _: androidx.core.widget.NestedScrollView, _: Int, _: Int, _: Int, _: Int ->
hideKeyboard()
}

// Also hide on touch
setOnTouchListener { v, event ->
if (event.action == MotionEvent.ACTION_DOWN) {
v.hideKeyboard()
}
false // Don't consume the event
}
}

/**
* Sets up touch listener to hide keyboard when clicking outside of SearchView
*
* Note: SuppressLint is used because we're not handling clicks - we're only hiding the keyboard
* and letting the event propagate normally (returning false). This is a UX enhancement, not
* an accessibility feature, so performClick() is not needed here.
*/
@SuppressLint("ClickableViewAccessibility")
internal fun View.setupHideKeyboardOnTouch() {
// Skip if not a container
if (this !is ViewGroup) return

setOnTouchListener { v, event ->
if (event.action == MotionEvent.ACTION_DOWN) {
// Check if the touch is outside any SearchView
if (!isTouchInsideSearchView(this, event)) {
v.hideKeyboard()
v.clearFocus()
}
}
false // Don't consume the event, let it propagate
}
}

/**
* Recursively checks if the touch event is inside a SearchView or its children
*/
private fun isTouchInsideSearchView(viewGroup: ViewGroup, event: MotionEvent): Boolean {
for (i in 0 until viewGroup.childCount) {
val child = viewGroup.getChildAt(i)

if (child is SearchView) {
// Check if touch is within SearchView bounds
val location = IntArray(2)
child.getLocationOnScreen(location)
val x = location[0]
val y = location[1]

if (event.rawX >= x && event.rawX <= x + child.width &&
event.rawY >= y && event.rawY <= y + child.height
) {
return true
}
}

if (child is ViewGroup) {
if (isTouchInsideSearchView(child, event)) {
return true
}
}
}
return false
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import android.graphics.drawable.GradientDrawable
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.trendyol.android.devtools.analyticslogger.R
import com.trendyol.android.devtools.analyticslogger.databinding.AnalyticsLoggerItemEventBinding
import com.trendyol.android.devtools.analyticslogger.internal.domain.model.Event
import com.trendyol.android.devtools.analyticslogger.internal.factory.ColorFactory
import com.trendyol.android.devtools.analyticslogger.internal.ui.model.EventItemViewState

internal class EventAdapter : PagingDataAdapter<Event, EventAdapter.EventViewHolder>(
diffCallback = object : DiffUtil.ItemCallback<Event>() {
Expand All @@ -28,6 +30,7 @@ internal class EventAdapter : PagingDataAdapter<Event, EventAdapter.EventViewHol
) {

var onItemSelected: ((event: Event) -> Unit)? = null
var searchQuery: String? = null

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EventViewHolder {
return EventViewHolder(
Expand Down Expand Up @@ -58,12 +61,20 @@ internal class EventAdapter : PagingDataAdapter<Event, EventAdapter.EventViewHol
fun bind(event: Event) = with(binding) {
boundItem = event

textViewKey.text = event.key
// Create ViewState - all presentation logic is inside
val viewState = EventItemViewState(event, searchQuery.orEmpty())

// ViewState handles all the logic!
textViewKey.text = viewState.getKeyText()
textViewSource.text = event.source
textViewPlatform.text = event.platform
textViewDate.text = event.date
textViewPlatform.background = createPlatformBackground(event.platform)
root.background = createStatusBackground(root.context, event.isSuccess)

// ViewState handles visibility and text logic
textViewBodyPreview.isVisible = viewState.isBodyPreviewVisible
textViewBodyPreview.text = viewState.getBodyPreviewText()
}

private fun createPlatformBackground(platform: String?): GradientDrawable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,9 @@ internal class MainViewModel(
private val eventManager: EventManager,
) : ViewModel() {

init {
viewModelScope.launch {
_platformsState.value = eventManager.getPlatforms()
}
}
private val _queryState = MutableStateFlow<String>("")
val queryState: StateFlow<String> = _queryState

private val queryState = MutableStateFlow<String?>("")
private val platformState = MutableStateFlow("")

private val _detailState = MutableStateFlow<DetailState>(DetailState.Initial)
Expand All @@ -34,20 +30,35 @@ internal class MainViewModel(
private val _platformsState = MutableStateFlow<List<String>>(emptyList())
val platformsState: StateFlow<List<String>> = _platformsState

init {
viewModelScope.launch {
_platformsState.value = eventManager.getPlatforms()
}
}

val eventsFlow: Flow<PagingData<Event>> = Pager(PagingConfig(pageSize = PAGE_SIZE)) {
EventPagingSource(
eventManager = eventManager,
query = queryState.value,
query = _queryState.value,
platform = platformState.value
)
}
.flow
.cachedIn(viewModelScope)

fun setQuery(query: String?) {
queryState.value = query.orEmpty()
/**
* Updates the search query
* This is the main entry point for search functionality
*/
fun setQuery(query: String) {
_queryState.value = query
}

/**
* Gets the current search query
*/
fun getQuery(): String = _queryState.value

fun setFilterState(platform: String) {
platformState.value = if (platform == "All") "" else platform
}
Expand Down
Loading