LeakScreen.kt
TLDR
This file contains the LeakScreen
class, which is responsible for displaying information about a memory leak. It retrieves the leak data from a database and populates the UI with the relevant information.
Classes
LeakScreen
The LeakScreen
class is a screen that displays details about a memory leak. It retrieves the leak information from the database and updates the UI with the relevant data. It also handles user interactions such as selecting different leak traces and performing actions like sharing or printing.
Methods
The LeakScreen
class contains the following methods:
createView(container: ViewGroup): View
Creates the view for the LeakScreen
screen. It inflates the layout file and sets up the UI components. It also retrieves the leak data from the database and updates the UI accordingly.
onLeaksRetrieved(leak: LeakProjection, selectedLeakTraceIndex: Int, selectedHeapAnalysis: HeapAnalysisSuccess)
Handles the case when leak data is retrieved from the database. Updates the UI components with the leak information and sets up event listeners for user interactions.
bindSimpleRow(view: View, leakTrace: LeakTraceProjection)
Binds the data of a leak trace to a UI row in the list view. Updates the UI components with the relevant information.
parseLinks(str: String): String
Parses the input string and converts URLs into HTML links. Returns the parsed string.
onLeakTraceSelected(analysis: HeapAnalysisSuccess, heapAnalysisId: Long, leakTraceIndex: Int)
Handles the case when a leak trace is selected by the user. Updates the UI components with the selected leak trace information and sets up event listeners for user interactions.
leakToString(leakTrace: LeakTrace, analysis: HeapAnalysisSuccess): String
Converts the given leak trace and analysis information into a formatted string. Returns the formatted string.
END
package leakcanary.internal.activity.screen
import android.text.Html
import android.text.SpannableStringBuilder
import android.util.Patterns
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.AdapterView.OnItemSelectedListener
import android.widget.ListView
import android.widget.Spinner
import android.widget.TextView
import com.squareup.leakcanary.core.R
import leakcanary.internal.DisplayLeakAdapter
import leakcanary.internal.SquigglySpan
import leakcanary.internal.activity.db.HeapAnalysisTable
import leakcanary.internal.activity.db.LeakTable
import leakcanary.internal.activity.db.LeakTable.LeakProjection
import leakcanary.internal.activity.db.LeakTable.LeakTraceProjection
import leakcanary.internal.activity.db.executeOnDb
import leakcanary.internal.activity.share
import leakcanary.internal.activity.shareHeapDump
import leakcanary.internal.activity.shareToStackOverflow
import leakcanary.internal.activity.ui.SimpleListAdapter
import leakcanary.internal.activity.ui.TimeFormatter
import leakcanary.internal.activity.ui.UiUtils.replaceUrlSpanWithAction
import leakcanary.internal.navigation.Screen
import leakcanary.internal.navigation.activity
import leakcanary.internal.navigation.goTo
import leakcanary.internal.navigation.inflate
import shark.HeapAnalysisSuccess
import shark.LeakTrace
import shark.LibraryLeak
import shark.SharkLog
internal class LeakScreen(
private val leakSignature: String,
private val selectedHeapAnalysisId: Long? = null
) : Screen() {
override fun createView(container: ViewGroup) =
container.inflate(R.layout.leak_canary_leak_screen)
.apply {
activity.title = resources.getString(R.string.leak_canary_loading_title)
executeOnDb {
val leak = LeakTable.retrieveLeakBySignature(db, leakSignature)
if (leak == null) {
updateUi {
activity.title = resources.getString(R.string.leak_canary_leak_not_found)
}
} else {
val selectedLeakIndex =
if (selectedHeapAnalysisId == null) 0 else leak.leakTraces.indexOfFirst { it.heapAnalysisId == selectedHeapAnalysisId }
if (selectedLeakIndex != -1) {
val heapAnalysisId = leak.leakTraces[selectedLeakIndex].heapAnalysisId
val selectedHeapAnalysis =
HeapAnalysisTable.retrieve<HeapAnalysisSuccess>(db, heapAnalysisId)!!
updateUi {
onLeaksRetrieved(leak, selectedLeakIndex, selectedHeapAnalysis)
}
} else {
// This can happen if a delete was enqueued and is slow and the user tapped on a leak
// row before the deletion is perform and the UI update that leaves the screen
// executes.
updateUi {
activity.title = "Selected heap analysis deleted"
}
}
LeakTable.markAsRead(db, leakSignature)
}
}
}
private fun View.onLeaksRetrieved(
leak: LeakProjection,
selectedLeakTraceIndex: Int,
selectedHeapAnalysis: HeapAnalysisSuccess
) {
val isLibraryLeak = leak.isLibraryLeak
val isNew = leak.isNew
val newChipView = findViewById<TextView>(R.id.leak_canary_chip_new)
val libraryLeakChipView = findViewById<TextView>(R.id.leak_canary_chip_library_leak)
newChipView.visibility = if (isNew) View.VISIBLE else View.GONE
libraryLeakChipView.visibility = if (isLibraryLeak) View.VISIBLE else View.GONE
activity.title = String.format(
resources.getQuantityText(
R.plurals.leak_canary_group_screen_title, leak.leakTraces.size
)
.toString(), leak.leakTraces.size, leak.shortDescription
)
val singleLeakTraceRow = findViewById<View>(R.id.leak_canary_single_leak_trace_row)
val spinner = findViewById<Spinner>(R.id.leak_canary_spinner)
if (leak.leakTraces.size == 1) {
spinner.visibility = View.GONE
val leakTrace = leak.leakTraces.first()
bindSimpleRow(singleLeakTraceRow, leakTrace)
onLeakTraceSelected(selectedHeapAnalysis, leakTrace.heapAnalysisId, leakTrace.leakTraceIndex)
} else {
singleLeakTraceRow.visibility = View.GONE
spinner.adapter =
SimpleListAdapter(R.layout.leak_canary_simple_row, leak.leakTraces) { view, position ->
bindSimpleRow(view, leak.leakTraces[position])
}
var lastSelectedLeakTraceIndex = selectedLeakTraceIndex
var lastSelectedHeapAnalysis = selectedHeapAnalysis
spinner.onItemSelectedListener = object : OnItemSelectedListener {
override fun onNothingSelected(parent: AdapterView<*>?) {
}
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
val selectedLeakTrace = leak.leakTraces[position]
val selectedHeapAnalysisId = selectedLeakTrace.heapAnalysisId
val lastSelectedHeapAnalysisId =
leak.leakTraces[lastSelectedLeakTraceIndex].heapAnalysisId
if (selectedHeapAnalysisId != lastSelectedHeapAnalysisId) {
executeOnDb {
val newSelectedHeapAnalysis =
HeapAnalysisTable.retrieve<HeapAnalysisSuccess>(db, selectedHeapAnalysisId)!!
updateUi {
lastSelectedLeakTraceIndex = position
lastSelectedHeapAnalysis = newSelectedHeapAnalysis
onLeakTraceSelected(
newSelectedHeapAnalysis, selectedHeapAnalysisId,
selectedLeakTrace.leakTraceIndex
)
}
}
} else {
lastSelectedLeakTraceIndex = position
onLeakTraceSelected(
lastSelectedHeapAnalysis, selectedHeapAnalysisId, selectedLeakTrace.leakTraceIndex
)
}
}
}
spinner.setSelection(selectedLeakTraceIndex)
}
}
private fun bindSimpleRow(
view: View,
leakTrace: LeakTraceProjection
) {
val titleView = view.findViewById<TextView>(R.id.leak_canary_row_text)
val timeView = view.findViewById<TextView>(R.id.leak_canary_row_small_text)
titleView.text =
view.resources.getString(R.string.leak_canary_class_has_leaked, leakTrace.classSimpleName)
timeView.text = TimeFormatter.formatTimestamp(view.context, leakTrace.createdAtTimeMillis)
}
private fun parseLinks(str: String): String {
val words = str.split(" ")
var parsedString = ""
for (word in words) {
parsedString += if (Patterns.WEB_URL.matcher(word)
.matches()
) {
"<a href=\"${word}\">${word}</a>"
} else {
word
}
if (words.indexOf(word) != words.size - 1) parsedString += " "
}
return parsedString
}
private fun View.onLeakTraceSelected(
analysis: HeapAnalysisSuccess,
heapAnalysisId: Long,
leakTraceIndex: Int
) {
val selectedLeak = analysis.allLeaks.first { it.signature == leakSignature }
val leakTrace = selectedLeak.leakTraces[leakTraceIndex]
val listView = findViewById<ListView>(R.id.leak_canary_list)
listView.alpha = 0f
listView.animate()
.alpha(1f)
val titleText = """
Open <a href="open_analysis">Heap Dump</a><br><br>
Share leak trace <a href="share">as text</a> or on <a href="share_stack_overflow">Stack Overflow</a><br><br>
Print leak trace <a href="print">to Logcat</a> (tag: LeakCanary)<br><br>
Share <a href="share_hprof">Heap Dump file</a><br><br>
References <b><u>underlined</u></b> are the likely causes of the leak.
Learn more at <a href="https://squ.re/leaks">https://squ.re/leaks</a>
""".trimIndent() + if (selectedLeak is LibraryLeak) "<br><br>" +
"A <font color='#FFCC32'>Library Leak</font> is a leak caused by a known bug in 3rd party code that you do not have control over. " +
"(<a href=\"https://square.github.io/leakcanary/fundamentals-how-leakcanary-works/#4-categorizing-leaks\">Learn More</a>)<br><br>" +
"<b>Leak pattern</b>: ${selectedLeak.pattern}<br><br>" +
"<b>Description</b>: ${parseLinks(selectedLeak.description)}" else ""
val title = Html.fromHtml(titleText) as SpannableStringBuilder
SquigglySpan.replaceUnderlineSpans(title, context)
replaceUrlSpanWithAction(title) { urlSpan ->
when (urlSpan) {
"share" -> {
{
share(LeakTraceWrapper.wrap(leakToString(leakTrace, analysis), 80))
}
}
"share_stack_overflow" -> {
{
shareToStackOverflow(LeakTraceWrapper.wrap(leakToString(leakTrace, analysis), 80))
}
}
"print" -> {
{
SharkLog.d {
"\u200B\n" + LeakTraceWrapper.wrap(
leakToString(leakTrace, analysis), 120
)
}
}
}
"open_analysis" -> {
{
goTo(HeapDumpScreen(heapAnalysisId))
}
}
"share_hprof" -> {
{
shareHeapDump(analysis.heapDumpFile)
}
}
else -> null
}
}
val adapter = DisplayLeakAdapter(context, leakTrace, title)
listView.adapter = adapter
}
private fun leakToString(
leakTrace: LeakTrace,
analysis: HeapAnalysisSuccess
) = """$leakTrace
METADATA
${
if (analysis.metadata.isNotEmpty()) {
analysis.metadata
.map { "${it.key}: ${it.value}" }
.joinToString("\n")
} else {
""
}
}
Analysis duration: ${analysis.analysisDurationMillis} ms"""
}