<lambda>null1 package leakcanary.internal.activity.screen
2
3 import android.text.Html
4 import android.text.SpannableStringBuilder
5 import android.util.Patterns
6 import android.view.View
7 import android.view.ViewGroup
8 import android.widget.AdapterView
9 import android.widget.AdapterView.OnItemSelectedListener
10 import android.widget.ListView
11 import android.widget.Spinner
12 import android.widget.TextView
13 import com.squareup.leakcanary.core.R
14 import leakcanary.internal.DisplayLeakAdapter
15 import leakcanary.internal.SquigglySpan
16 import leakcanary.internal.activity.db.HeapAnalysisTable
17 import leakcanary.internal.activity.db.LeakTable
18 import leakcanary.internal.activity.db.LeakTable.LeakProjection
19 import leakcanary.internal.activity.db.LeakTable.LeakTraceProjection
20 import leakcanary.internal.activity.db.executeOnDb
21 import leakcanary.internal.activity.share
22 import leakcanary.internal.activity.shareHeapDump
23 import leakcanary.internal.activity.shareToStackOverflow
24 import leakcanary.internal.activity.ui.SimpleListAdapter
25 import leakcanary.internal.activity.ui.TimeFormatter
26 import leakcanary.internal.activity.ui.UiUtils.replaceUrlSpanWithAction
27 import leakcanary.internal.navigation.Screen
28 import leakcanary.internal.navigation.activity
29 import leakcanary.internal.navigation.goTo
30 import leakcanary.internal.navigation.inflate
31 import shark.HeapAnalysisSuccess
32 import shark.LeakTrace
33 import shark.LibraryLeak
34 import shark.SharkLog
35
36 internal class LeakScreen(
37 private val leakSignature: String,
38 private val selectedHeapAnalysisId: Long? = null
39 ) : Screen() {
40 override fun createView(container: ViewGroup) =
41 container.inflate(R.layout.leak_canary_leak_screen)
42 .apply {
43 activity.title = resources.getString(R.string.leak_canary_loading_title)
44 executeOnDb {
45 val leak = LeakTable.retrieveLeakBySignature(db, leakSignature)
46
47 if (leak == null) {
48 updateUi {
49 activity.title = resources.getString(R.string.leak_canary_leak_not_found)
50 }
51 } else {
52 val selectedLeakIndex =
53 if (selectedHeapAnalysisId == null) 0 else leak.leakTraces.indexOfFirst { it.heapAnalysisId == selectedHeapAnalysisId }
54
55 if (selectedLeakIndex != -1) {
56 val heapAnalysisId = leak.leakTraces[selectedLeakIndex].heapAnalysisId
57 val selectedHeapAnalysis =
58 HeapAnalysisTable.retrieve<HeapAnalysisSuccess>(db, heapAnalysisId)!!
59
60 updateUi {
61 onLeaksRetrieved(leak, selectedLeakIndex, selectedHeapAnalysis)
62 }
63 } else {
64 // This can happen if a delete was enqueued and is slow and the user tapped on a leak
65 // row before the deletion is perform and the UI update that leaves the screen
66 // executes.
67 updateUi {
68 activity.title = "Selected heap analysis deleted"
69 }
70 }
71 LeakTable.markAsRead(db, leakSignature)
72 }
73 }
74 }
75
76 private fun View.onLeaksRetrieved(
77 leak: LeakProjection,
78 selectedLeakTraceIndex: Int,
79 selectedHeapAnalysis: HeapAnalysisSuccess
80 ) {
81 val isLibraryLeak = leak.isLibraryLeak
82 val isNew = leak.isNew
83 val newChipView = findViewById<TextView>(R.id.leak_canary_chip_new)
84 val libraryLeakChipView = findViewById<TextView>(R.id.leak_canary_chip_library_leak)
85 newChipView.visibility = if (isNew) View.VISIBLE else View.GONE
86 libraryLeakChipView.visibility = if (isLibraryLeak) View.VISIBLE else View.GONE
87
88 activity.title = String.format(
89 resources.getQuantityText(
90 R.plurals.leak_canary_group_screen_title, leak.leakTraces.size
91 )
92 .toString(), leak.leakTraces.size, leak.shortDescription
93 )
94
95 val singleLeakTraceRow = findViewById<View>(R.id.leak_canary_single_leak_trace_row)
96 val spinner = findViewById<Spinner>(R.id.leak_canary_spinner)
97
98 if (leak.leakTraces.size == 1) {
99 spinner.visibility = View.GONE
100
101 val leakTrace = leak.leakTraces.first()
102
103 bindSimpleRow(singleLeakTraceRow, leakTrace)
104 onLeakTraceSelected(selectedHeapAnalysis, leakTrace.heapAnalysisId, leakTrace.leakTraceIndex)
105 } else {
106 singleLeakTraceRow.visibility = View.GONE
107
108 spinner.adapter =
109 SimpleListAdapter(R.layout.leak_canary_simple_row, leak.leakTraces) { view, position ->
110 bindSimpleRow(view, leak.leakTraces[position])
111 }
112
113 var lastSelectedLeakTraceIndex = selectedLeakTraceIndex
114 var lastSelectedHeapAnalysis = selectedHeapAnalysis
115
116 spinner.onItemSelectedListener = object : OnItemSelectedListener {
117 override fun onNothingSelected(parent: AdapterView<*>?) {
118 }
119
120 override fun onItemSelected(
121 parent: AdapterView<*>?,
122 view: View?,
123 position: Int,
124 id: Long
125 ) {
126 val selectedLeakTrace = leak.leakTraces[position]
127
128 val selectedHeapAnalysisId = selectedLeakTrace.heapAnalysisId
129 val lastSelectedHeapAnalysisId =
130 leak.leakTraces[lastSelectedLeakTraceIndex].heapAnalysisId
131
132 if (selectedHeapAnalysisId != lastSelectedHeapAnalysisId) {
133 executeOnDb {
134 val newSelectedHeapAnalysis =
135 HeapAnalysisTable.retrieve<HeapAnalysisSuccess>(db, selectedHeapAnalysisId)!!
136 updateUi {
137 lastSelectedLeakTraceIndex = position
138 lastSelectedHeapAnalysis = newSelectedHeapAnalysis
139 onLeakTraceSelected(
140 newSelectedHeapAnalysis, selectedHeapAnalysisId,
141 selectedLeakTrace.leakTraceIndex
142 )
143 }
144 }
145 } else {
146 lastSelectedLeakTraceIndex = position
147 onLeakTraceSelected(
148 lastSelectedHeapAnalysis, selectedHeapAnalysisId, selectedLeakTrace.leakTraceIndex
149 )
150 }
151 }
152 }
153 spinner.setSelection(selectedLeakTraceIndex)
154 }
155 }
156
157 private fun bindSimpleRow(
158 view: View,
159 leakTrace: LeakTraceProjection
160 ) {
161 val titleView = view.findViewById<TextView>(R.id.leak_canary_row_text)
162 val timeView = view.findViewById<TextView>(R.id.leak_canary_row_small_text)
163
164 titleView.text =
165 view.resources.getString(R.string.leak_canary_class_has_leaked, leakTrace.classSimpleName)
166 timeView.text = TimeFormatter.formatTimestamp(view.context, leakTrace.createdAtTimeMillis)
167 }
168
169 private fun parseLinks(str: String): String {
170 val words = str.split(" ")
171 var parsedString = ""
172 for (word in words) {
173 parsedString += if (Patterns.WEB_URL.matcher(word)
174 .matches()
175 ) {
176 "<a href=\"${word}\">${word}</a>"
177 } else {
178 word
179 }
180 if (words.indexOf(word) != words.size - 1) parsedString += " "
181 }
182 return parsedString
183 }
184
185 private fun View.onLeakTraceSelected(
186 analysis: HeapAnalysisSuccess,
187 heapAnalysisId: Long,
188 leakTraceIndex: Int
189 ) {
190 val selectedLeak = analysis.allLeaks.first { it.signature == leakSignature }
191 val leakTrace = selectedLeak.leakTraces[leakTraceIndex]
192
193 val listView = findViewById<ListView>(R.id.leak_canary_list)
194 listView.alpha = 0f
195 listView.animate()
196 .alpha(1f)
197
198 val titleText = """
199 Open <a href="open_analysis">Heap Dump</a><br><br>
200 Share leak trace <a href="share">as text</a> or on <a href="share_stack_overflow">Stack Overflow</a><br><br>
201 Print leak trace <a href="print">to Logcat</a> (tag: LeakCanary)<br><br>
202 Share <a href="share_hprof">Heap Dump file</a><br><br>
203 References <b><u>underlined</u></b> are the likely causes of the leak.
204 Learn more at <a href="https://squ.re/leaks">https://squ.re/leaks</a>
205 """.trimIndent() + if (selectedLeak is LibraryLeak) "<br><br>" +
206 "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. " +
207 "(<a href=\"https://square.github.io/leakcanary/fundamentals-how-leakcanary-works/#4-categorizing-leaks\">Learn More</a>)<br><br>" +
208 "<b>Leak pattern</b>: ${selectedLeak.pattern}<br><br>" +
209 "<b>Description</b>: ${parseLinks(selectedLeak.description)}" else ""
210
211 val title = Html.fromHtml(titleText) as SpannableStringBuilder
212 SquigglySpan.replaceUnderlineSpans(title, context)
213
214 replaceUrlSpanWithAction(title) { urlSpan ->
215 when (urlSpan) {
216 "share" -> {
217 {
218 share(LeakTraceWrapper.wrap(leakToString(leakTrace, analysis), 80))
219 }
220 }
221 "share_stack_overflow" -> {
222 {
223 shareToStackOverflow(LeakTraceWrapper.wrap(leakToString(leakTrace, analysis), 80))
224 }
225 }
226 "print" -> {
227 {
228 SharkLog.d {
229 "\u200B\n" + LeakTraceWrapper.wrap(
230 leakToString(leakTrace, analysis), 120
231 )
232 }
233 }
234 }
235 "open_analysis" -> {
236 {
237 goTo(HeapDumpScreen(heapAnalysisId))
238 }
239 }
240 "share_hprof" -> {
241 {
242 shareHeapDump(analysis.heapDumpFile)
243 }
244 }
245 else -> null
246 }
247 }
248
249 val adapter = DisplayLeakAdapter(context, leakTrace, title)
250 listView.adapter = adapter
251 }
252
253 private fun leakToString(
254 leakTrace: LeakTrace,
255 analysis: HeapAnalysisSuccess
256 ) = """$leakTrace
257
258 METADATA
259
260 ${
261 if (analysis.metadata.isNotEmpty()) {
262 analysis.metadata
263 .map { "${it.key}: ${it.value}" }
264 .joinToString("\n")
265 } else {
266 ""
267 }
268 }
269 Analysis duration: ${analysis.analysisDurationMillis} ms"""
270 }
271