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
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ import com.itsaky.androidide.ui.ContentTranslatingDrawerLayout
import com.itsaky.androidide.ui.MemoryUsageChartRenderer
import com.itsaky.androidide.ui.MetricsCarouselAdapter
import com.itsaky.androidide.ui.MetricsPage
import com.itsaky.androidide.ui.NetworkUsageChartRenderer
import com.itsaky.androidide.ui.SwipeRevealLayout
import com.itsaky.androidide.uidesigner.UIDesignerActivity
import com.itsaky.androidide.utils.ActionMenuUtils.showPopupWindow
Expand All @@ -131,6 +132,7 @@ import com.itsaky.androidide.utils.FlashType
import com.itsaky.androidide.utils.InstallationResultHandler.onResult
import com.itsaky.androidide.utils.IntentUtils
import com.itsaky.androidide.utils.MemoryUsageWatcher
import com.itsaky.androidide.utils.NetworkUsageWatcher
import com.itsaky.androidide.utils.StringsInjectionException
import com.itsaky.androidide.utils.StringsXmlInjector
import com.itsaky.androidide.utils.applyBottomSheetAnchorForOrientation
Expand Down Expand Up @@ -195,6 +197,15 @@ abstract class BaseEditorActivity :
lineColorFor = Companion::getMemUsageLineColorFor,
)

private val networkUsageWatcher = NetworkUsageWatcher()
private val networkUsageChartRenderer =
NetworkUsageChartRenderer(usageProvider = networkUsageWatcher::getUsage)

private val networkUsageListener =
NetworkUsageWatcher.NetworkUsageListener { usage ->
networkUsageChartRenderer.onUsageChanged(usage)
}

private val fileManagerViewModel by viewModels<FileManagerViewModel>()
private var feedbackButtonManager: FeedbackButtonManager? = null
private var fullscreenManager: FullscreenManager? = null
Expand Down Expand Up @@ -546,11 +557,15 @@ abstract class BaseEditorActivity :
metricsPageCallback = null
_binding?.memUsageView?.metricsPager?.adapter = null
memUsageChartRenderer.detach()
networkUsageChartRenderer.detach()
_binding = null

if (isDestroying) {
memoryUsageWatcher.stopWatching(true)
memoryUsageWatcher.listener = null
// close(), not stopWatching(): this is the terminal teardown, and the watcher holds a
// dedicated sampling thread that newSingleThreadContext keeps alive until it is closed.
networkUsageWatcher.close()
editorActivityScope.cancelIfActive("Activity is being destroyed")

unbindDebuggerService()
Expand Down Expand Up @@ -999,18 +1014,14 @@ abstract class BaseEditorActivity :
private fun setupMetricsCarousel() {
val pages =
listOf(
// The memory chart is the default page (ADFA-5487). The logo is a placeholder second
// page until there is a real second metric; the network-traffic chart replaces it.
// The memory chart is the default page (ADFA-5487); network traffic is the second
// (ADFA-5489), replacing the brand-mark placeholder that ADFA-5487 shipped.
MetricsPage.MemoryChart(title = string.metrics_title_memory),
MetricsPage.Image(
drawable = R.drawable.cogo_brand_mark,
description = string.metrics_carousel_brand_mark,
// The product's own name, from the one place it is defined.
title = string.app_name,
),
MetricsPage.NetworkChart(title = string.metrics_title_network),
)

binding.memUsageView.metricsPager.adapter = MetricsCarouselAdapter(pages, memUsageChartRenderer)
binding.memUsageView.metricsPager.adapter =
MetricsCarouselAdapter(pages, memUsageChartRenderer, networkUsageChartRenderer)

val showTitleFor = { position: Int ->
pages.getOrNull(position)?.let { page ->
Expand Down Expand Up @@ -1047,6 +1058,8 @@ abstract class BaseEditorActivity :
super.onPause()
memoryUsageWatcher.listener = null
memoryUsageWatcher.stopWatching(false)
networkUsageWatcher.listener = null
networkUsageWatcher.stopWatching()

this.isDestroying = isFinishing
getFileTreeFragment()?.saveTreeState()
Expand All @@ -1063,8 +1076,17 @@ abstract class BaseEditorActivity :
log.warn("Unable to move debugger overlay to display {}", displayId, err)
}

memoryUsageWatcher.listener = memoryUsageListener
memoryUsageWatcher.startWatching()
// Not for an instance onCreate already abandoned: the deep-link path calls finish() and
// returns, yet the platform still runs onStart and onResume. The memory watcher is immune
// by design -- it early-returns on an empty process set -- but the network sampler would
// poll TrafficStats and hop to the main thread once a second for an activity with no
// chart to render into.
if (didCompleteLiveOnCreate) {
memoryUsageWatcher.listener = memoryUsageListener
memoryUsageWatcher.startWatching()
networkUsageWatcher.listener = networkUsageListener
networkUsageWatcher.startWatching()
}

apkInstallationViewModel.reloadStatus(this)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ package com.itsaky.androidide.ui
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.recyclerview.widget.RecyclerView
import com.itsaky.androidide.R
Expand All @@ -40,10 +38,8 @@ sealed interface MetricsPage {
@StringRes override val title: Int,
) : MetricsPage

/** A static image. Placeholder page until real metrics exist to show alongside memory. */
data class Image(
@DrawableRes val drawable: Int,
@StringRes val description: Int,
/** The live network-traffic chart, rendered by [NetworkUsageChartRenderer]. */
data class NetworkChart(
@StringRes override val title: Int,
) : MetricsPage
}
Expand All @@ -54,13 +50,14 @@ sealed interface MetricsPage {
* [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.
*
* The chart page holds no sample state of its own: [chartRenderer] is attached when the page binds
* and detached when it is recycled, and rebuilds the full history from [MemoryUsageChartRenderer]'s
* watcher each time. Swiping away from the chart and back therefore loses nothing.
* 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 chartRenderer: MemoryUsageChartRenderer,
private val memoryChartRenderer: MemoryUsageChartRenderer,
private val networkChartRenderer: NetworkUsageChartRenderer,
) : RecyclerView.Adapter<MetricsCarouselAdapter.PageViewHolder>() {
sealed class PageViewHolder(
view: View,
Expand All @@ -69,17 +66,17 @@ class MetricsCarouselAdapter(
val chart: SafeLineChart,
) : PageViewHolder(chart)

class Image(
val image: ImageView,
) : PageViewHolder(image)
class NetworkChart(
val chart: SafeLineChart,
) : PageViewHolder(chart)
}

override fun getItemCount(): Int = pages.size

override fun getItemViewType(position: Int): Int =
when (pages[position]) {
is MetricsPage.MemoryChart -> VIEW_TYPE_MEMORY_CHART
is MetricsPage.Image -> VIEW_TYPE_IMAGE
is MetricsPage.NetworkChart -> VIEW_TYPE_NETWORK_CHART
}

override fun onCreateViewHolder(
Expand All @@ -94,9 +91,9 @@ class MetricsCarouselAdapter(
)
}

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

Expand All @@ -110,31 +107,29 @@ class MetricsCarouselAdapter(
holder: PageViewHolder,
position: Int,
) {
when (val page = pages[position]) {
when (pages[position]) {
is MetricsPage.MemoryChart -> {
chartRenderer.attach((holder as PageViewHolder.MemoryChart).chart)
memoryChartRenderer.attach((holder as PageViewHolder.MemoryChart).chart)
}

is MetricsPage.Image -> {
(holder as PageViewHolder.Image).image.apply {
setImageResource(page.drawable)
contentDescription = context.getString(page.description)
}
is MetricsPage.NetworkChart -> {
networkChartRenderer.attach((holder as PageViewHolder.NetworkChart).chart)
}
}
}

override fun onViewRecycled(holder: PageViewHolder) {
if (holder is PageViewHolder.MemoryChart) {
// 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.
chartRenderer.detachIfAttached(holder.chart)
// 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_IMAGE = 1
const val VIEW_TYPE_NETWORK_CHART = 1
}
}
Loading
Loading