Skip to content

Commit 313ed8a

Browse files
authored
[feat] add network request logging and its list UI (#74)
* add network request logging and its list UI * add configure method in DebugOverlay for customization
1 parent fa249f2 commit 313ed8a

File tree

33 files changed

+836
-101
lines changed

33 files changed

+836
-101
lines changed

build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ plugins {
1111
alias(libs.plugins.androidLibrary) apply false
1212
alias(libs.plugins.kotlin.android) apply false
1313
alias(libs.plugins.kotlin.compose) apply false
14+
alias(libs.plugins.hilt.android) apply false
15+
alias(libs.plugins.ksp) apply false
1416
alias(libs.plugins.dexcount) apply false
1517
alias(libs.plugins.mavenPublish) apply false
1618
alias(libs.plugins.detekt) apply false

debugoverlay-core/src/main/kotlin/com/ms/square/debugoverlay/DebugOverlay.kt

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.ms.square.debugoverlay
33
import android.app.Application
44
import androidx.annotation.MainThread
55
import androidx.annotation.RestrictTo
6+
import com.ms.square.debugoverlay.internal.Logger
67
import com.ms.square.debugoverlay.internal.OverlayViewManager
78
import com.ms.square.debugoverlay.internal.data.DebugOverlayDataRepository
89
import com.ms.square.debugoverlay.internal.util.checkMainThread
@@ -13,16 +14,24 @@ import kotlinx.coroutines.SupervisorJob
1314

1415
/**
1516
* Internal entry point for DebugOverlay auto-installers.
16-
* This is called automatically on app startup by either:
17+
* [DebugOverlay.install] is called automatically on app startup by either:
1718
* - DebugOverlayInstaller (ContentProvider) in the default debugoverlay artifact
1819
* - DebugOverlayStartupInitializer (AndroidX Startup) in the debugoverlay-androidx-startup artifact
1920
*
20-
* **Not intended for use by application code.** There is no supported way to manually control
21-
* the overlay lifecycle in v2.x.
21+
* ** [DebugOverlay.install] is Not intended for use by application code.**
22+
* There is no supported way to manually control the overlay lifecycle in v2.x.
2223
*/
23-
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
2424
public object DebugOverlay {
2525

26+
private var config: Config = Config()
27+
set(newConfig) {
28+
if (field != newConfig) {
29+
field = newConfig
30+
_overlayDataRepository?.setNetworkTracker(newConfig.networkRequestTracker)
31+
?: Logger.d("Config updated before install, will apply during install")
32+
}
33+
}
34+
2635
private var _overlayDataRepository: DebugOverlayDataRepository? = null
2736

2837
private var overlayScope: CoroutineScope? = null
@@ -36,6 +45,7 @@ public object DebugOverlay {
3645
internal val overlayDataRepository: DebugOverlayDataRepository
3746
get() = _overlayDataRepository ?: error("DebugOverlayDataRepository not initialized")
3847

48+
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
3949
@MainThread
4050
public fun install(application: Application) {
4151
if (!isMainProcess(application)) {
@@ -46,8 +56,52 @@ public object DebugOverlay {
4656
check(!isInstalled) { "DebugOverlay already installed" }
4757

4858
overlayScope = CoroutineScope(SupervisorJob() + Dispatchers.Default).also {
49-
_overlayDataRepository = DebugOverlayDataRepository(it)
59+
_overlayDataRepository = DebugOverlayDataRepository(it).apply {
60+
setNetworkTracker(config.networkRequestTracker)
61+
}
5062
overlayViewManager = OverlayViewManager(application, it)
5163
}
5264
}
65+
66+
/**
67+
* Configures DebugOverlay settings. Must be called on the main thread.
68+
*
69+
* Auto-installation happens via ContentProvider before [Application.onCreate].
70+
* Call this function in [Application.onCreate] after dependency injection to
71+
* configure network tracking or other features.
72+
*
73+
* Example with Hilt:
74+
* ```kotlin
75+
* @HiltAndroidApp
76+
* class MyApp : Application() {
77+
* @Inject lateinit var networkInterceptor: DebugOverlayNetworkInterceptor
78+
*
79+
* override fun onCreate() {
80+
* super.onCreate()
81+
* DebugOverlay.configure {
82+
* copy(networkRequestTracker = networkInterceptor)
83+
* }
84+
* }
85+
* }
86+
* ```
87+
*
88+
* @param block Configuration builder that receives current [Config] and returns new [Config]
89+
* @throws IllegalStateException if called from non-main thread
90+
*/
91+
@MainThread
92+
public fun configure(block: Config.() -> Config) {
93+
checkMainThread()
94+
config = config.block()
95+
}
96+
97+
/**
98+
* DebugOverlay configuration.
99+
*
100+
* @property networkRequestTracker Tracks HTTP requests for display in Network tab.
101+
* Default is [NoOpNetworkRequestTracker] which disables network tracking.
102+
* Use DebugOverlayNetworkInterceptor from debugoverlay-extension-okhttp for OkHttp integration.
103+
*
104+
* @see configure
105+
*/
106+
public data class Config(val networkRequestTracker: NetworkRequestTracker = NoOpNetworkRequestTracker)
53107
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.ms.square.debugoverlay
2+
3+
import com.ms.square.debugoverlay.model.NetworkRequest
4+
import kotlinx.coroutines.flow.Flow
5+
import kotlinx.coroutines.flow.flowOf
6+
7+
/**
8+
* Interface for tracking network requests.
9+
*
10+
* This abstraction allows the debug panel to work with any HTTP client
11+
* (OkHttp, Ktor, Retrofit, custom clients) without being tied to a specific implementation.
12+
*
13+
* Usage:
14+
* ```kotlin
15+
* // Implement for your HTTP client
16+
* class MyNetworkTracker : NetworkRequestTracker {
17+
* private val _requests = MutableStateFlow<List<NetworkRequest>>(emptyList())
18+
* override val requests: Flow<List<NetworkRequest>> = _requests.asStateFlow()
19+
* }
20+
*
21+
* // Use in DebugOverlay
22+
* DebugOverlay.config =
23+
* DebugOverlay.config.copy(networkRequestTracker = debugOverlayNetworkInterceptor)
24+
* ```
25+
*/
26+
public interface NetworkRequestTracker {
27+
28+
/**
29+
* Flow of network request logs, with newest log at the end.
30+
*
31+
* Implementations should:
32+
* - Emit a new list whenever a request completes
33+
* - Limit stored requests (e.g., last 100)
34+
*/
35+
public val requests: Flow<List<NetworkRequest>>
36+
}
37+
38+
/**
39+
* No-op implementation for when tracking is disabled.
40+
*/
41+
public object NoOpNetworkRequestTracker : NetworkRequestTracker {
42+
override val requests: Flow<List<NetworkRequest>> = flowOf(emptyList())
43+
}

debugoverlay-core/src/main/kotlin/com/ms/square/debugoverlay/internal/data/DebugOverlayDataRepository.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
package com.ms.square.debugoverlay.internal.data
22

3+
import com.ms.square.debugoverlay.NetworkRequestTracker
4+
import com.ms.square.debugoverlay.NoOpNetworkRequestTracker
35
import com.ms.square.debugoverlay.internal.data.model.LogcatEntry
46
import com.ms.square.debugoverlay.internal.data.model.NetworkStats
57
import com.ms.square.debugoverlay.internal.data.source.LogcatDataSource
68
import com.ms.square.debugoverlay.internal.data.source.NetStatsDataSource
9+
import com.ms.square.debugoverlay.model.NetworkRequest
710
import kotlinx.coroutines.CoroutineScope
11+
import kotlinx.coroutines.ExperimentalCoroutinesApi
812
import kotlinx.coroutines.awaitCancellation
913
import kotlinx.coroutines.flow.Flow
14+
import kotlinx.coroutines.flow.MutableStateFlow
15+
import kotlinx.coroutines.flow.flatMapLatest
1016
import kotlinx.coroutines.launch
1117

1218
internal class DebugOverlayDataRepository(scope: CoroutineScope) {
1319

20+
private val currentNetworkRequestTracker = MutableStateFlow<NetworkRequestTracker>(NoOpNetworkRequestTracker)
1421
private val logcatDataSource = LogcatDataSource(scope)
1522
private val netStatsDataSource = NetStatsDataSource(scope)
1623

@@ -29,4 +36,12 @@ internal class DebugOverlayDataRepository(scope: CoroutineScope) {
2936

3037
val logs: Flow<List<LogcatEntry>> = logcatDataSource.logs
3138
val netStats: Flow<NetworkStats> = netStatsDataSource.stats
39+
40+
@OptIn(ExperimentalCoroutinesApi::class)
41+
val networkRequests: Flow<List<NetworkRequest>> = currentNetworkRequestTracker
42+
.flatMapLatest { tracker -> tracker.requests }
43+
44+
fun setNetworkTracker(tracker: NetworkRequestTracker) {
45+
currentNetworkRequestTracker.value = tracker
46+
}
3247
}

debugoverlay-core/src/main/kotlin/com/ms/square/debugoverlay/internal/data/model/NetworkStats.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ package com.ms.square.debugoverlay.internal.data.model
66
internal data class NetworkStats(
77
val totalDownloaded: Long, // Total bytes downloaded
88
val totalUploaded: Long, // Total bytes uploaded
9+
val totalRequests: Int? = null, // Total number of requests
10+
val errorCount: Int? = null, // Requests with 4xx/5xx status
11+
val avgDuration: Long? = null, // Average duration in ms
912
) {
1013
companion object {
1114
val INITIAL_VALUE: NetworkStats = NetworkStats(0, 0)

debugoverlay-core/src/main/kotlin/com/ms/square/debugoverlay/internal/ui/LogEntryDetailBottomSheet.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ private fun DetailHeader(onDismiss: () -> Unit, modifier: Modifier = Modifier) {
133133

134134
@Composable
135135
private fun DetailMetadataRow(logEntry: LogcatEntry, modifier: Modifier = Modifier) {
136+
val levelColor = logEntry.level.toColor()
136137
Row(
137138
modifier = modifier
138139
.fillMaxWidth()
@@ -148,13 +149,13 @@ private fun DetailMetadataRow(logEntry: LogcatEntry, modifier: Modifier = Modifi
148149
)
149150
Surface(
150151
shape = RoundedCornerShape(12.dp),
151-
color = logEntry.level.toColor().copy(alpha = 0.2f)
152+
color = levelColor.copy(alpha = 0.2f)
152153
) {
153154
Text(
154155
text = logEntry.level.name,
155156
style = MaterialTheme.typography.labelMedium,
156157
fontWeight = FontWeight.Bold,
157-
color = logEntry.level.toColor(),
158+
color = levelColor,
158159
modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp)
159160
)
160161
}
@@ -171,7 +172,7 @@ private fun DetailMetadataRow(logEntry: LogcatEntry, modifier: Modifier = Modifi
171172
text = logEntry.tag,
172173
style = MaterialTheme.typography.bodyMedium,
173174
fontFamily = FontFamily.Monospace,
174-
color = logEntry.level.toColor(),
175+
color = levelColor,
175176
fontWeight = FontWeight.SemiBold
176177
)
177178
}

debugoverlay-core/src/main/kotlin/com/ms/square/debugoverlay/internal/ui/LogcatTabContent.kt

Lines changed: 1 addition & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,11 @@ import androidx.compose.foundation.lazy.rememberLazyListState
2525
import androidx.compose.foundation.shape.RoundedCornerShape
2626
import androidx.compose.material.icons.Icons
2727
import androidx.compose.material.icons.filled.ArrowDownward
28-
import androidx.compose.material.icons.filled.Clear
29-
import androidx.compose.material.icons.filled.Search
3028
import androidx.compose.material3.FloatingActionButton
3129
import androidx.compose.material3.Icon
32-
import androidx.compose.material3.IconButton
3330
import androidx.compose.material3.MaterialTheme
34-
import androidx.compose.material3.OutlinedTextField
3531
import androidx.compose.material3.Surface
3632
import androidx.compose.material3.Text
37-
import androidx.compose.material3.TextFieldDefaults
3833
import androidx.compose.runtime.Composable
3934
import androidx.compose.runtime.getValue
4035
import androidx.compose.runtime.mutableStateOf
@@ -154,6 +149,7 @@ private fun LogcatFilterBar(
154149
) {
155150
Column(modifier = modifier.fillMaxWidth()) {
156151
SearchField(
152+
searchPlaceholder = stringResource(R.string.debugoverlay_search_logs),
157153
searchQuery = searchQuery,
158154
onSearchQueryChanged = onSearchQueryChanged
159155
)
@@ -402,46 +398,3 @@ private fun FilterChip(
402398
)
403399
}
404400
}
405-
406-
@Composable
407-
private fun SearchField(searchQuery: String, onSearchQueryChanged: (String) -> Unit, modifier: Modifier = Modifier) {
408-
OutlinedTextField(
409-
value = searchQuery,
410-
onValueChange = onSearchQueryChanged,
411-
modifier = modifier
412-
.fillMaxWidth()
413-
.padding(horizontal = 16.dp, vertical = 8.dp),
414-
placeholder = {
415-
Text(
416-
text = stringResource(R.string.debugoverlay_search_logs),
417-
style = MaterialTheme.typography.bodyMedium
418-
)
419-
},
420-
leadingIcon = {
421-
Icon(
422-
imageVector = Icons.Default.Search,
423-
contentDescription = null,
424-
tint = MaterialTheme.colorScheme.onSurfaceVariant
425-
)
426-
},
427-
trailingIcon = {
428-
if (searchQuery.isNotEmpty()) {
429-
IconButton(onClick = { onSearchQueryChanged("") }) {
430-
Icon(
431-
imageVector = Icons.Default.Clear,
432-
contentDescription = stringResource(R.string.debugoverlay_clear_search),
433-
tint = MaterialTheme.colorScheme.onSurfaceVariant
434-
)
435-
}
436-
}
437-
},
438-
singleLine = true,
439-
shape = RoundedCornerShape(12.dp),
440-
colors = TextFieldDefaults.colors(
441-
focusedContainerColor = MaterialTheme.colorScheme.surfaceContainerHighest,
442-
unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainerHighest,
443-
focusedIndicatorColor = Color.Transparent,
444-
unfocusedIndicatorColor = Color.Transparent
445-
)
446-
)
447-
}

0 commit comments

Comments
 (0)