• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2015 Square, Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package leakcanary.internal
17 
18 import android.content.Context
19 import android.text.Html
20 import android.text.SpannableStringBuilder
21 import android.text.method.LinkMovementMethod
22 import android.view.View
23 import android.view.ViewGroup
24 import android.widget.BaseAdapter
25 import android.widget.TextView
26 import com.squareup.leakcanary.core.R
27 import leakcanary.internal.DisplayLeakConnectorView.Type
28 import leakcanary.internal.DisplayLeakConnectorView.Type.END
29 import leakcanary.internal.DisplayLeakConnectorView.Type.END_FIRST_UNREACHABLE
30 import leakcanary.internal.DisplayLeakConnectorView.Type.GC_ROOT
31 import leakcanary.internal.DisplayLeakConnectorView.Type.NODE_FIRST_UNREACHABLE
32 import leakcanary.internal.DisplayLeakConnectorView.Type.NODE_LAST_REACHABLE
33 import leakcanary.internal.DisplayLeakConnectorView.Type.NODE_REACHABLE
34 import leakcanary.internal.DisplayLeakConnectorView.Type.NODE_UNKNOWN
35 import leakcanary.internal.DisplayLeakConnectorView.Type.NODE_UNREACHABLE
36 import leakcanary.internal.DisplayLeakConnectorView.Type.START
37 import leakcanary.internal.DisplayLeakConnectorView.Type.START_LAST_REACHABLE
38 import leakcanary.internal.navigation.getColorCompat
39 import leakcanary.internal.navigation.inflate
40 import leakcanary.internal.utils.humanReadableByteCount
41 import shark.LeakTrace
42 import shark.LeakTrace.GcRootType.JAVA_FRAME
43 import shark.LeakTraceObject
44 import shark.LeakTraceObject.LeakingStatus.LEAKING
45 import shark.LeakTraceObject.LeakingStatus.NOT_LEAKING
46 import shark.LeakTraceObject.LeakingStatus.UNKNOWN
47 import shark.LeakTraceReference
48 import shark.LeakTraceReference.ReferenceType.INSTANCE_FIELD
49 import shark.LeakTraceReference.ReferenceType.STATIC_FIELD
50 
51 @Suppress("DEPRECATION")
52 internal class DisplayLeakAdapter constructor(
53   context: Context,
54   private val leakTrace: LeakTrace,
55   private val header: CharSequence
56 ) : BaseAdapter() {
57 
58   private val highlightColorHexString: String =
59     hexStringColor(context, R.color.leak_canary_class_name)
60   private val leakColorHexString: String = hexStringColor(context, R.color.leak_canary_leak)
61   private val referenceColorHexString: String =
62     hexStringColor(context, R.color.leak_canary_reference)
63   private val extraColorHexString: String = hexStringColor(context, R.color.leak_canary_extra)
64   private val helpColorHexString: String = hexStringColor(context, R.color.leak_canary_help)
65 
66   override fun getView(
67     position: Int,
68     convertView: View?,
69     parent: ViewGroup
70   ): View {
71     return when (getItemViewType(position)) {
72       HEADER_ROW -> {
73         val view = convertView ?: parent.inflate(R.layout.leak_canary_leak_header)
74         bindHeaderRow(view)
75         view
76       }
77       CONNECTOR_ROW -> {
78         val view = convertView ?: parent.inflate(R.layout.leak_canary_ref_row)
79         bindConnectorRow(view, position)
80         view
81       }
82       else -> {
83         throw IllegalStateException("Unexpected type ${getItemViewType(position)}")
84       }
85     }
86   }
87 
88   private fun bindHeaderRow(
89     view: View
90   ) {
91     view.findViewById<TextView>(R.id.leak_canary_header_text)
92       .apply {
93         movementMethod = LinkMovementMethod.getInstance()
94         text = header
95       }
96   }
97 
98   private fun bindConnectorRow(
99     view: View,
100     position: Int
101   ) {
102     val titleView = view.findViewById<TextView>(R.id.leak_canary_row_title)
103     val connector = view.findViewById<DisplayLeakConnectorView>(R.id.leak_canary_row_connector)
104 
105     connector.setType(getConnectorType(position))
106 
107     titleView.text = when {
108       position == 1 -> {
109         "GC Root: ${leakTrace.gcRootType.description}"
110       }
111       position < count - 1 -> {
112         val referencePathIndex = elementIndex(position)
113         val referencePath = leakTrace.referencePath[referencePathIndex]
114         val isSuspect = leakTrace.referencePathElementIsSuspect(referencePathIndex)
115 
116         val leakTraceObject = referencePath.originObject
117 
118         val typeName =
119           if (position == 2 && leakTrace.gcRootType == JAVA_FRAME) "thread" else leakTraceObject.typeName
120 
121         var referenceName = referencePath.referenceDisplayName
122 
123         referenceName = referenceName.replace("<".toRegex(), "&lt;")
124           .replace(">".toRegex(), "&gt;")
125 
126         referenceName = if (isSuspect) {
127           "<u><font color='$leakColorHexString'>$referenceName</font></u>"
128         } else {
129           "<font color='$referenceColorHexString'>$referenceName</font>"
130         }
131 
132         if (referencePath.referenceType == STATIC_FIELD) {
133           referenceName = "<i>$referenceName</i>"
134         }
135 
136         if (isSuspect) {
137           referenceName = "<b>$referenceName</b>"
138         }
139 
140         val staticPrefix = if (referencePath.referenceType == STATIC_FIELD) "static " else ""
141 
142         val htmlString = leakTraceObject.asHtmlString(typeName) +
143           "$INDENTATION$staticPrefix${referencePath.styledOwningClassSimpleName()}${
144             when (referencePath.referenceType) {
145               STATIC_FIELD, INSTANCE_FIELD -> "."
146               else -> ""
147             }
148           }$referenceName"
149 
150         val builder = Html.fromHtml(htmlString) as SpannableStringBuilder
151         if (isSuspect) {
152           SquigglySpan.replaceUnderlineSpans(builder, view.context)
153         }
154         builder
155       }
156       else -> {
157         Html.fromHtml(leakTrace.leakingObject.asHtmlString(leakTrace.leakingObject.typeName))
158       }
159     }
160   }
161 
162   private fun LeakTraceObject.asHtmlString(typeName: String): String {
163     val packageEnd = className.lastIndexOf('.')
164 
165     val extra: (String) -> String = { "<font color='$extraColorHexString'>$it</font>" }
166 
167     val styledClassName = styledClassSimpleName()
168     var htmlString =
169       if (packageEnd != -1) "${
170         extra(
171           className.substring(
172             0, packageEnd
173           )
174         )
175       }.$styledClassName" else styledClassName
176     htmlString += " ${extra(typeName)}<br>"
177 
178     val reachabilityString = when (leakingStatus) {
179       UNKNOWN -> extra("UNKNOWN")
180       NOT_LEAKING -> "NO" + extra(" (${leakingStatusReason})")
181       LEAKING -> "YES" + extra(" (${leakingStatusReason})")
182     }
183 
184     htmlString += "$INDENTATION${extra("Leaking: ")}$reachabilityString<br>"
185 
186     retainedHeapByteSize?.let {
187       val humanReadableRetainedHeapSize = humanReadableByteCount(it.toLong(), si = true)
188       htmlString += "${INDENTATION}${extra("Retaining ")}$humanReadableRetainedHeapSize${
189         extra(
190           " in "
191         )
192       }$retainedObjectCount${extra(" objects")}<br>"
193     }
194 
195     labels.forEach { label ->
196       htmlString += "$INDENTATION${extra(label)}<br>"
197     }
198     return htmlString
199   }
200 
201   private fun LeakTraceObject.styledClassSimpleName(): String {
202     val simpleName = classSimpleName.replace("[]", "[ ]")
203     return "<font color='$highlightColorHexString'>$simpleName</font>"
204   }
205 
206   private fun LeakTraceReference.styledOwningClassSimpleName(): String {
207     val simpleName = owningClassSimpleName.removeSuffix("[]")
208     return "<font color='$highlightColorHexString'>$simpleName</font>"
209   }
210 
211   @Suppress("ReturnCount")
212   private fun getConnectorType(position: Int): Type {
213     if (position == 1) {
214       return GC_ROOT
215     } else if (position == 2) {
216       return when (leakTrace.referencePath.size) {
217         0 -> END_FIRST_UNREACHABLE
218         1 -> START_LAST_REACHABLE
219         else -> {
220           val nextReachability = leakTrace.referencePath[1].originObject
221           if (nextReachability.leakingStatus != NOT_LEAKING) {
222             START_LAST_REACHABLE
223           } else START
224         }
225       }
226     } else {
227       val isLeakingInstance = position == count - 1
228       if (isLeakingInstance) {
229         val previousReachability = leakTrace.referencePath.last()
230           .originObject
231         return if (previousReachability.leakingStatus != LEAKING) {
232           END_FIRST_UNREACHABLE
233         } else END
234       } else {
235         val reachability = leakTrace.referencePath[elementIndex(position)].originObject
236         when (reachability.leakingStatus) {
237           UNKNOWN -> return NODE_UNKNOWN
238           NOT_LEAKING -> {
239             val nextReachability =
240               if (position + 1 == count - 1) leakTrace.leakingObject else leakTrace.referencePath[elementIndex(
241                 position + 1
242               )].originObject
243             return if (nextReachability.leakingStatus != NOT_LEAKING) {
244               NODE_LAST_REACHABLE
245             } else {
246               NODE_REACHABLE
247             }
248           }
249           LEAKING -> {
250             val previousReachability =
251               leakTrace.referencePath[elementIndex(position - 1)].originObject
252             return if (previousReachability.leakingStatus != LEAKING) {
253               NODE_FIRST_UNREACHABLE
254             } else {
255               NODE_UNREACHABLE
256             }
257           }
258           else -> throw IllegalStateException(
259             "Unknown value: " + reachability.leakingStatus
260           )
261         }
262       }
263     }
264   }
265 
266   override fun isEnabled(position: Int) = false
267 
268   override fun getCount() = leakTrace.referencePath.size + 3
269 
270   override fun getItem(position: Int) = when (position) {
271       0, 1 -> null
272       count - 1 -> leakTrace.leakingObject
273       else -> leakTrace.referencePath[elementIndex(position)]
274   }
275 
276   private fun elementIndex(position: Int) = position - 2
277 
278   override fun getViewTypeCount() = 2
279 
280   override fun getItemViewType(position: Int) = if (position == 0) HEADER_ROW else CONNECTOR_ROW
281 
282   override fun getItemId(position: Int) = position.toLong()
283 
284   companion object {
285 
286     const val HEADER_ROW = 0
287     const val CONNECTOR_ROW = 1
288     val INDENTATION = "&nbsp;".repeat(4)
289 
290     // https://stackoverflow.com/a/6540378/703646
291     private fun hexStringColor(
292       context: Context,
293       colorResId: Int
294     ): String {
295       return String.format("#%06X", 0xFFFFFF and context.getColorCompat(colorResId))
296     }
297   }
298 }
299