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(), "<") 124 .replace(">".toRegex(), ">") 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 = " ".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