Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ccc3ae2
feat(metrics): add a temperature and power page to the carousel (ADFA…
davidschachterADFA Sep 5, 2026
7f34c6e
feat(metrics): colour the throttle bands, label power in watts, stagg…
davidschachterADFA Sep 6, 2026
59d6851
fix(metrics): put the sampling-rate tap on the edge the x axis is dra…
davidschachterADFA Sep 6, 2026
b53677d
Merge branch 'feature/ADFA-5486-chart-improvements' into feature/ADFA…
davidschachterADFA Sep 6, 2026
7a92309
Merge branch 'feature/ADFA-5486-chart-improvements' into feature/ADFA…
davidschachterADFA Sep 6, 2026
5028834
docs(metrics): correct the sign contract on the battery current (ADFA…
davidschachterADFA Sep 6, 2026
34353ea
Merge remote-tracking branch 'origin/feature/ADFA-5499-power-chart' i…
davidschachterADFA Sep 6, 2026
9116990
Merge branch 'feature/ADFA-5486-chart-improvements' into feature/ADFA…
davidschachterADFA Sep 6, 2026
a524280
Merge branch 'feature/ADFA-5486-chart-improvements' into feature/ADFA…
davidschachterADFA Sep 6, 2026
4b03b6e
Merge branch 'feature/ADFA-5486-chart-improvements' into feature/ADFA…
davidschachterADFA Sep 6, 2026
521564b
Merge branch 'feature/ADFA-5486-chart-improvements' into feature/ADFA…
davidschachterADFA Sep 6, 2026
c0ddf1f
Merge branch 'feature/ADFA-5486-chart-improvements' into feature/ADFA…
davidschachterADFA Sep 6, 2026
cfd67a6
Merge branch 'feature/ADFA-5486-chart-improvements' into feature/ADFA…
davidschachterADFA Sep 6, 2026
9f15dd8
fix(metrics): range the power axes, and give the watcher its siblings…
davidschachterADFA Sep 6, 2026
14aae3f
ADFA-5499: address review findings on the power page
davidschachterADFA Sep 6, 2026
7b50282
ADFA-5499: keep the battery readout off the topmost axis label
davidschachterADFA Sep 6, 2026
a569642
Merge remote-tracking branch 'origin/feature/ADFA-5486-chart-improvem…
davidschachterADFA Sep 6, 2026
6596564
ADFA-5499: make the carousel adapter actually page-agnostic
davidschachterADFA Sep 6, 2026
4341746
ADFA-5499: set axis bounds before the notify, and publish watcher state
davidschachterADFA Sep 6, 2026
f9119a4
Merge branch 'feature/ADFA-5486-chart-improvements' into feature/ADFA…
davidschachterADFA Sep 6, 2026
935fae8
Merge branch 'feature/ADFA-5486-chart-improvements' into feature/ADFA…
davidschachterADFA Sep 6, 2026
6525024
Merge branch 'feature/ADFA-5486-chart-improvements' into feature/ADFA…
davidschachterADFA Sep 7, 2026
fb99a91
Merge branch 'feature/ADFA-5486-chart-improvements' into feature/ADFA…
davidschachterADFA Sep 7, 2026
5d89cea
Merge branch 'feature/ADFA-5486-chart-improvements' into feature/ADFA…
davidschachterADFA Sep 7, 2026
a3c90ad
ADFA-5499: hide the battery readout when the carousel undocks
davidschachterADFA Sep 8, 2026
6246453
ADFA-5499: fix the re-review findings on the power page
davidschachterADFA Sep 8, 2026
ba6c3d0
Merge branch 'feature/ADFA-5486-chart-improvements' into feature/ADFA…
davidschachterADFA Sep 8, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -196,10 +196,13 @@ abstract class BaseEditorActivity :

protected val networkUsageWatcher get() = metricsViewModel.networkUsageWatcher

protected val powerUsageWatcher get() = metricsViewModel.powerUsageWatcher

protected val metricsCarousel by lazy {
MetricsCarouselController(
memoryUsageWatcher = memoryUsageWatcher,
networkUsageWatcher = networkUsageWatcher,
powerUsageWatcher = powerUsageWatcher,
lineColorFor = Companion::getMemUsageLineColorFor,
annotations = metricsViewModel.annotations,
)
Expand Down Expand Up @@ -569,6 +572,11 @@ abstract class BaseEditorActivity :
// recreation, so it must not be torn down whenever this activity goes away.
memoryUsageWatcher.listener = null
networkUsageWatcher.listener = null
// The third one too. It was missed when the power page was added, and only
// metricsCarousel.unbind() a few lines above was releasing it -- under an identity
// check, and skipped entirely for an undocked carousel. Asymmetry here is what hides
// which watcher is holding a dead controller.
powerUsageWatcher.listener = null
editorActivityScope.cancelIfActive("Activity is being destroyed")

unbindDebuggerService()
Expand Down Expand Up @@ -1112,6 +1120,9 @@ abstract class BaseEditorActivity :
if (!networkUsageWatcher.isWatching) {
networkUsageWatcher.startWatching()
}
if (!powerUsageWatcher.isWatching) {
powerUsageWatcher.startWatching()
}

if (!isMetricsCarouselUndocked()) {
// Draw whatever was sampled while away, rather than waiting for the next tick.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,7 @@ class MemoryUsageChartRenderer(
}
}

applyAxisRange(chart, processes)
setData(chart, datasets)
setData(chart, datasets) { applyAxisRange(it, processes) }
}

/**
Expand Down Expand Up @@ -190,10 +189,11 @@ class MemoryUsageChartRenderer(
if (dataChanged) {
// From the samples already in hand: usagesProvider() copies every history, so calling
// it again here would snapshot the whole buffer a second time per tick.
applyAxisRangeFor(chart) { visit ->
memoryUsage.forEachValue { visit(it) }
redraw(chart) { ranged ->
applyAxisRangeFor(ranged) { visit ->
memoryUsage.forEachValue { visit(it) }
}
}
redraw(chart)
}
}

Expand Down
124 changes: 59 additions & 65 deletions app/src/main/java/com/itsaky/androidide/ui/MetricsCarouselAdapter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
package com.itsaky.androidide.ui

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.StringRes
import androidx.recyclerview.widget.RecyclerView
Expand All @@ -27,109 +26,104 @@ import com.itsaky.androidide.R
/**
* A page of the editor's metrics carousel.
*
* A page says what it is called, what it is, and what draws it. Nothing else in the carousel needs
* to know which page it is holding, which is what lets [MetricsCarouselAdapter] be page-agnostic.
*
* Deliberately an ordinary interface rather than a sealed one. The adapter has always claimed that
* a new display -- including one contributed by a plugin -- could be added without touching it;
* while this was sealed that was impossible, since a plugin is a different module and could not
* implement it at all.
*
* @property title Names the page. Shown below the carousel, and the only cue to which page is
* showing, so every page needs one.
* @property contentDescription What the plot is, for a screen reader.
* @property renderer Draws this page and owns its axes, annotations and shading.
*/
sealed interface MetricsPage {
interface MetricsPage {
@get:StringRes val title: Int

/** The live memory-usage chart, rendered by [MemoryUsageChartRenderer]. */
data class MemoryChart(
@StringRes override val title: Int,
) : MetricsPage
@get:StringRes val contentDescription: Int

/** The live network-traffic chart, rendered by [NetworkUsageChartRenderer]. */
data class NetworkChart(
@StringRes override val title: Int,
) : MetricsPage
val renderer: MetricsChartRenderer
}

/**
* A page showing one line chart.
*
* There used to be a type per metric -- `MemoryChart`, `NetworkChart`, `PowerChart` -- each with a
* layout of its own that differed from its siblings by one attribute, plus a view type, a view
* holder subclass and a branch in four `when` expressions. They differed in nothing a chart page
* needs to differ in.
*/
data class ChartPage(
@StringRes override val title: Int,
@StringRes override val contentDescription: Int,
override val renderer: MetricsChartRenderer,
) : MetricsPage

/**
* Backs the editor's horizontally swipeable carousel of metric displays.
*
* [pages] is a constructor argument rather than a hardcoded list so that new displays -- a network
* traffic chart, or pages contributed by plugins -- can be added without touching this class.
* [pages] is a constructor argument rather than a hardcoded list so that new displays can be added
* without touching this class -- and now nothing here names a page or a metric, so that is true
* rather than aspirational.
*
* A chart page holds no sample state of its own: its renderer is attached when the page binds and
* detached when it is recycled, and rebuilds the full history from its watcher each time. Moving
* away from a chart and back therefore loses nothing.
*/
class MetricsCarouselAdapter(
private val pages: List<MetricsPage>,
private val memoryChartRenderer: MemoryUsageChartRenderer,
private val networkChartRenderer: NetworkUsageChartRenderer,
) : RecyclerView.Adapter<MetricsCarouselAdapter.PageViewHolder>() {
sealed class PageViewHolder(
view: View,
) : RecyclerView.ViewHolder(view) {
class MemoryChart(
val chart: SafeLineChart,
) : PageViewHolder(chart)

class NetworkChart(
val chart: SafeLineChart,
) : PageViewHolder(chart)
/**
* @property boundRenderer What was attached to [chart] at bind time, so [onViewRecycled] can
* detach the right renderer without being told the position -- which it is not.
*/
class PageViewHolder(
val chart: SafeLineChart,
) : RecyclerView.ViewHolder(chart) {
var boundRenderer: MetricsChartRenderer? = null
}

override fun getItemCount(): Int = pages.size

override fun getItemViewType(position: Int): Int =
when (pages[position]) {
is MetricsPage.MemoryChart -> VIEW_TYPE_MEMORY_CHART
is MetricsPage.NetworkChart -> VIEW_TYPE_NETWORK_CHART
}
/**
* One view type per page, so a chart is never recycled from one page onto another.
*
* Not a saving worth making here: a chart carries the state its renderer put on it, and some of
* that is written by one renderer and cleared by none of the others -- the thermal shading on
* the power page is set through [SafeLineChart.backgroundSpans], which a memory or network
* renderer has no reason to touch. A handful of pages, each keeping its own chart, costs
* nothing and cannot leak one page's decoration onto another.
*/
override fun getItemViewType(position: Int): Int = position

override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
): PageViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
VIEW_TYPE_MEMORY_CHART -> {
PageViewHolder.MemoryChart(
inflater.inflate(R.layout.item_metrics_memory_chart, parent, false) as SafeLineChart,
)
}

VIEW_TYPE_NETWORK_CHART -> {
PageViewHolder.NetworkChart(
inflater.inflate(R.layout.item_metrics_network_chart, parent, false) as SafeLineChart,
)
}

else -> {
throw IllegalArgumentException("Unknown metrics page view type: $viewType")
}
}
val chart =
LayoutInflater
.from(parent.context)
.inflate(R.layout.item_metrics_chart, parent, false) as SafeLineChart
return PageViewHolder(chart)
}

override fun onBindViewHolder(
holder: PageViewHolder,
position: Int,
) {
when (pages[position]) {
is MetricsPage.MemoryChart -> {
memoryChartRenderer.attach((holder as PageViewHolder.MemoryChart).chart)
}

is MetricsPage.NetworkChart -> {
networkChartRenderer.attach((holder as PageViewHolder.NetworkChart).chart)
}
}
val page = pages[position]
holder.chart.contentDescription = holder.chart.context.getString(page.contentDescription)
holder.boundRenderer = page.renderer
page.renderer.attach(holder.chart)
}

override fun onViewRecycled(holder: PageViewHolder) {
// Only if this holder's chart is still the attached one: a rebind can create the replacement
// before RecyclerView recycles the view it replaced, and detaching then would drop the new
// chart instead of the old.
when (holder) {
is PageViewHolder.MemoryChart -> memoryChartRenderer.detachIfAttached(holder.chart)
is PageViewHolder.NetworkChart -> networkChartRenderer.detachIfAttached(holder.chart)
}
}

private companion object {
const val VIEW_TYPE_MEMORY_CHART = 0
const val VIEW_TYPE_NETWORK_CHART = 1
holder.boundRenderer?.detachIfAttached(holder.chart)
holder.boundRenderer = null
}
}
Loading
Loading