main

square/leakcanary

Last updated at: 29/12/2023 09:38

HeapDumpScreen.kt

TLDR

This file contains the implementation of the HeapDumpScreen class, which is responsible for displaying information about a heap dump analysis. It shows a list of leaks found in the heap dump, allows the user to perform various actions on the analysis, and provides options for exploring and sharing the heap dump file.

Classes

HeapDumpScreen

The HeapDumpScreen class is responsible for displaying information about a heap dump analysis. It extends the Screen class and overrides the createView method to create and configure the view for the screen. The class has the following properties and methods:

  • analysisId: The ID of the heap analysis.
  • createView(container: ViewGroup): Creates and configures the view for the screen. This method inflates the layout file, sets the screen title to a loading message, and performs a database query to retrieve the heap analysis data. Depending on the result, it either shows the analysis data or displays a message indicating that the analysis has been deleted.
  • onSuccessRetrieved: Configures the screen UI when the heap analysis data is successfully retrieved. It sets the screen title to the analysis creation timestamp, adds menu items for deleting the analysis and rendering the heap dump, configures the list view adapter to display the list of leaks, and handles item clicks to navigate to the leak screen for a specific leak.
  • bindMetadataRow: Binds and configures the metadata row view in the list view adapter. It sets up click actions for hyperlinks in the metadata row, such as exploring the heap dump, sharing the analysis, printing the analysis to Logcat, sharing the heap dump file, and displaying metadata details.

Constants

  • METADATA: A constant representing the type of the metadata row in the list view adapter.
  • LEAK_TITLE: A constant representing the type of the leak title row in the list view adapter.
  • LEAK_ROW: A constant representing the type of a leak row in the list view adapter.
package leakcanary.internal.activity.screen

import android.R.drawable
import android.R.string
import android.app.ActivityManager
import android.app.AlertDialog.Builder
import android.text.Html
import android.text.SpannableStringBuilder
import android.text.method.LinkMovementMethod
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.ListView
import android.widget.TextView
import com.squareup.leakcanary.core.R
import leakcanary.internal.activity.db.HeapAnalysisTable
import leakcanary.internal.activity.db.LeakTable
import leakcanary.internal.activity.db.executeOnDb
import leakcanary.internal.activity.share
import leakcanary.internal.activity.shareHeapDump
import leakcanary.internal.activity.ui.TimeFormatter
import leakcanary.internal.activity.ui.UiUtils
import leakcanary.internal.navigation.Screen
import leakcanary.internal.navigation.activity
import leakcanary.internal.navigation.goBack
import leakcanary.internal.navigation.goTo
import leakcanary.internal.navigation.inflate
import leakcanary.internal.navigation.onCreateOptionsMenu
import shark.HeapAnalysis
import shark.HeapAnalysisSuccess
import shark.LibraryLeak
import shark.SharkLog

internal class HeapDumpScreen(
  private val analysisId: Long
) : Screen() {

  override fun createView(container: ViewGroup) =
    container.inflate(R.layout.leak_canary_list).apply {
      activity.title = resources.getString(R.string.leak_canary_loading_title)

      executeOnDb {
        val heapAnalysis = HeapAnalysisTable.retrieve<HeapAnalysisSuccess>(db, analysisId)
        if (heapAnalysis == null) {
          updateUi {
            activity.title = resources.getString(R.string.leak_canary_analysis_deleted_title)
          }
        } else {
          val signatures = heapAnalysis.allLeaks.map { it.signature }
            .toSet()
          val leakReadStatus = LeakTable.retrieveLeakReadStatuses(db, signatures)
          val heapDumpFileExist = heapAnalysis.heapDumpFile.exists()
          updateUi { onSuccessRetrieved(heapAnalysis, leakReadStatus, heapDumpFileExist) }
        }
      }
    }

  private fun View.onSuccessRetrieved(
    heapAnalysis: HeapAnalysisSuccess,
    leakReadStatus: Map<String, Boolean>,
    heapDumpFileExist: Boolean
  ) {

    activity.title = TimeFormatter.formatTimestamp(context, heapAnalysis.createdAtTimeMillis)

    onCreateOptionsMenu { menu ->
      if (!ActivityManager.isUserAMonkey()) {
        menu.add(R.string.leak_canary_delete)
          .setOnMenuItemClickListener {
            executeOnDb {
              HeapAnalysisTable.delete(db, analysisId, heapAnalysis.heapDumpFile)
              updateUi {
                goBack()
              }
            }
            true
          }
      }
      if (heapDumpFileExist) {
        menu.add(R.string.leak_canary_options_menu_render_heap_dump)
          .setOnMenuItemClickListener {
            goTo(RenderHeapDumpScreen(heapAnalysis.heapDumpFile))
            true
          }
      }
    }

    val listView = findViewById<ListView>(R.id.leak_canary_list)

    val leaks = heapAnalysis.allLeaks.sortedByDescending { it.leakTraces.size }
      .toList()

    listView.adapter = object : BaseAdapter() {
      override fun getView(
        position: Int,
        convertView: View?,
        parent: ViewGroup
      ) = when (getItemViewType(position)) {
        METADATA -> {
          bindMetadataRow(convertView, parent, heapDumpFileExist, heapAnalysis)
        }
        LEAK_TITLE -> {
          val view = convertView ?: parent.inflate(R.layout.leak_canary_heap_dump_leak_title)
          val leaksTextView = view.findViewById<TextView>(R.id.leak_canary_heap_dump_leaks)
          leaksTextView.text = resources.getQuantityString(
            R.plurals.leak_canary_distinct_leaks,
            leaks.size, leaks.size
          )
          view
        }
        LEAK_ROW -> {
          val view = convertView ?: parent.inflate(R.layout.leak_canary_leak_row)
          val countView = view.findViewById<TextView>(R.id.leak_canary_count_text)
          val descriptionView = view.findViewById<TextView>(R.id.leak_canary_leak_text)
          val timeView = view.findViewById<TextView>(R.id.leak_canary_time_text)
          val newChipView = view.findViewById<TextView>(R.id.leak_canary_chip_new)
          val libraryLeakChipView = view.findViewById<TextView>(R.id.leak_canary_chip_library_leak)

          val leak = leaks[position - 2]

          val isNew = !leakReadStatus.getValue(leak.signature)

          countView.isEnabled = isNew
          countView.text = leak.leakTraces.size.toString()
          newChipView.visibility = if (isNew) VISIBLE else GONE
          libraryLeakChipView.visibility = if (leak is LibraryLeak) VISIBLE else GONE
          descriptionView.text = leak.shortDescription

          val formattedDate =
            TimeFormatter.formatTimestamp(view.context, heapAnalysis.createdAtTimeMillis)
          timeView.text = formattedDate
          view
        }
        else -> {
          throw IllegalStateException("Unexpected type ${getItemViewType(position)}")
        }
      }

      override fun getItem(position: Int) = this

      override fun getItemId(position: Int) = position.toLong()

      override fun getCount() = 2 + leaks.size

      override fun getItemViewType(position: Int) = when (position) {
        0 -> METADATA
        1 -> LEAK_TITLE
        else -> LEAK_ROW
      }

      override fun getViewTypeCount() = 3

      override fun isEnabled(position: Int) = getItemViewType(position) == LEAK_ROW
    }

    listView.setOnItemClickListener { _, _, position, _ ->
      if (position > LEAK_TITLE) {
        goTo(LeakScreen(leaks[position - 2].signature, analysisId))
      }
    }
  }

  private fun View.bindMetadataRow(
    convertView: View?,
    parent: ViewGroup,
    heapDumpFileExist: Boolean,
    heapAnalysis: HeapAnalysisSuccess
  ): View {
    val view = convertView ?: parent.inflate(R.layout.leak_canary_leak_header)
    val textView = view.findViewById<TextView>(R.id.leak_canary_header_text)
    textView.movementMethod = LinkMovementMethod.getInstance()

    val explore =
      if (heapDumpFileExist) """Explore <a href="explore_hprof">Heap Dump</a><br><br>""" else ""
    val shareAnalysis = """Share <a href="share">Heap Dump analysis</a><br><br>"""
    val printAnalysis = """Print analysis <a href="print">to Logcat</a> (tag: LeakCanary)<br><br>"""
    val shareFile =
      if (heapDumpFileExist) """Share <a href="share_hprof">Heap Dump file</a><br><br>""" else ""

    val seeMetadata = "See <a href=\"metadata\">Metadata</a>"

    val dumpDurationMillis =
      if (heapAnalysis.dumpDurationMillis != HeapAnalysis.DUMP_DURATION_UNKNOWN) {
        "${heapAnalysis.dumpDurationMillis} ms"
      } else {
        "Unknown"
      }
    val metadata = (heapAnalysis.metadata + mapOf(
      "Analysis duration" to "${heapAnalysis.analysisDurationMillis} ms",
      "Heap dump file path" to heapAnalysis.heapDumpFile.absolutePath,
      "Heap dump timestamp" to "${heapAnalysis.createdAtTimeMillis}",
      "Heap dump duration" to dumpDurationMillis
    ))
      .map { "<b>${it.key}:</b> ${it.value}" }
      .joinToString("<br>")
    val titleText = explore + shareAnalysis + printAnalysis + shareFile + seeMetadata
    val title = Html.fromHtml(titleText) as SpannableStringBuilder

    UiUtils.replaceUrlSpanWithAction(title) { urlSpan ->
      when (urlSpan) {
        "explore_hprof" -> {
          {
            goTo(HprofExplorerScreen(heapAnalysis.heapDumpFile))
          }
        }
        "share" -> {
          {
            share(LeakTraceWrapper.wrap(heapAnalysis.toString(), 80))
          }
        }
        "print" -> {
          {
            SharkLog.d { "\u200B\n" + LeakTraceWrapper.wrap(heapAnalysis.toString(), 120) }
          }
        }
        "share_hprof" -> {
          {
            shareHeapDump(heapAnalysis.heapDumpFile)
          }
        }
        "metadata" -> {
          {
            Builder(context)
              .setIcon(drawable.ic_dialog_info)
              .setTitle("Metadata")
              .setMessage(Html.fromHtml(metadata))
              .setPositiveButton(string.ok, null)
              .show()
          }
        }
        else -> null
      }
    }

    textView.text = title
    return view
  }

  companion object {
    const val METADATA = 0
    const val LEAK_TITLE = 1
    const val LEAK_ROW = 2
  }
}