main

square/leakcanary

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

LeakTable.kt

TLDR

This file contains the implementation of the LeakTable class in the leakcanary.internal.activity.db package. The LeakTable class is responsible for managing the leak table in the database, including creating and dropping the table, inserting and retrieving data, and performing various operations on the table.

Methods

insert

This method is used to insert a new leak into the leak table in the database. It takes the database object, the heap analysis ID, and the leak object as parameters. The leak data is then inserted into the table, including the leak signature, short description, whether it is a library leak or not, and the read status. It also inserts the corresponding leak traces into the leak trace table.

retrieveLeakReadStatuses

This method is used to retrieve the read statuses of multiple leaks from the leak table in the database. It takes the database object and a set of leak signatures as parameters. It returns a map that contains the leak signature as the key and the read status as the value.

retrieveAllLeaks

This method is used to retrieve all the leaks from the leak table in the database. It returns a list of AllLeaksProjection objects, which contain information about each leak, including the leak signature, short description, creation time, number of leak traces, whether it is a library leak or not, and whether it is new or not.

markAsRead

This method is used to mark a specific leak as read in the leak table in the database. It takes the database object and the signature of the leak as parameters.

retrieveLeakBySignature

This method is used to retrieve a specific leak from the leak table in the database based on its signature. It takes the database object and the signature of the leak as parameters. It returns a LeakProjection object, which contains information about the leak, including the short description, read status, library leak status, and a list of leak trace projections.

deleteByHeapAnalysisId

This method is used to delete all the leaks associated with a specific heap analysis ID from the leak table in the database. It takes the database object and the heap analysis ID as parameters. It also deletes the corresponding leak traces from the leak trace table.

Classes

AllLeaksProjection

This class is a data class that represents a projection of all the leaks in the leak table. It contains properties such as the leak signature, short description, creation time, number of leak traces, library leak status, and new status.

LeakProjection

This class is a data class that represents a projection of a specific leak in the leak table. It contains properties such as the short description, new status, library leak status, and a list of leak trace projections.

LeakTraceProjection

This class is a data class that represents a projection of a leak trace associated with a specific leak in the leak table. It contains properties such as the leak trace index, heap analysis ID, class simple name, and creation time.

package leakcanary.internal.activity.db

import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import org.intellij.lang.annotations.Language
import shark.Leak
import shark.LibraryLeak

internal object LeakTable {

  @Language("RoomSql")
  const val create = """
        CREATE TABLE leak
        (
        id INTEGER PRIMARY KEY,
        signature TEXT UNIQUE,
        short_description TEXT,
        is_library_leak INTEGER,
        is_read INTEGER
        )"""

  @Language("RoomSql")
  const val createSignatureIndex = """
        CREATE INDEX leak_signature
        on leak (signature)
    """

  @Language("RoomSql")
  const val drop = "DROP TABLE IF EXISTS leak"

  fun insert(
    db: SQLiteDatabase,
    heapAnalysisId: Long,
    leak: Leak
  ): Long {
    val values = ContentValues()
    values.put("signature", leak.signature)
    values.put("short_description", leak.shortDescription)
    values.put("is_library_leak", if (leak is LibraryLeak) 1 else 0)
    values.put("is_read", 0)

    db.insertWithOnConflict("leak", null, values, SQLiteDatabase.CONFLICT_IGNORE)

    val leakId =
      db.rawQuery("SELECT id from leak WHERE signature = '${leak.signature}' LIMIT 1", null)
        .use { cursor ->
          if (cursor.moveToFirst()) cursor.getLong(0) else throw IllegalStateException(
            "No id found for leak with signature '${leak.signature}'"
          )
        }

    leak.leakTraces.forEachIndexed { index, leakTrace ->
      LeakTraceTable.insert(
        db = db,
        leakId = leakId,
        heapAnalysisId = heapAnalysisId,
        leakTraceIndex = index,
        leakingObjectClassSimpleName = leakTrace.leakingObject.classSimpleName
      )
    }

    return leakId
  }

  fun retrieveLeakReadStatuses(
    db: SQLiteDatabase,
    signatures: Set<String>
  ): Map<String, Boolean> {
    return db.rawQuery(
      """
      SELECT
      signature
      , is_read
      FROM leak
      WHERE signature IN (${signatures.joinToString { "'$it'" }})
    """, null
    )
      .use { cursor ->
        val leakReadStatuses = mutableMapOf<String, Boolean>()
        while (cursor.moveToNext()) {
          val signature = cursor.getString(0)
          val isRead = cursor.getInt(1) == 1
          leakReadStatuses[signature] = isRead
        }
        leakReadStatuses
      }
  }

  class AllLeaksProjection(
    val signature: String,
    val shortDescription: String,
    val createdAtTimeMillis: Long,
    val leakTraceCount: Int,
    val isLibraryLeak: Boolean,
    val isNew: Boolean
  )

  fun retrieveAllLeaks(
    db: SQLiteDatabase
  ): List<AllLeaksProjection> {
    return db.rawQuery(
      """
          SELECT
          l.signature
          , MIN(l.short_description)
          , MAX(h.created_at_time_millis) as created_at_time_millis
          , COUNT(*) as leak_trace_count
          , MIN(l.is_library_leak) as is_library_leak
          , MAX(l.is_read) as is_read
          FROM leak_trace lt
          LEFT JOIN leak l on lt.leak_id = l.id
          LEFT JOIN heap_analysis h ON lt.heap_analysis_id = h.id
          GROUP BY 1
          ORDER BY leak_trace_count DESC, created_at_time_millis DESC
          """, null
    )
      .use { cursor ->
        val all = mutableListOf<AllLeaksProjection>()
        while (cursor.moveToNext()) {
          val group = AllLeaksProjection(
            signature = cursor.getString(0),
            shortDescription = cursor.getString(1),
            createdAtTimeMillis = cursor.getLong(2),
            leakTraceCount = cursor.getInt(3),
            isLibraryLeak = cursor.getInt(4) == 1,
            isNew = cursor.getInt(5) == 0
          )
          all.add(group)
        }
        all
      }
  }

  fun markAsRead(
    db: SQLiteDatabase,
    signature: String
  ) {
    val values = ContentValues().apply { put("is_read", 1) }
    db.update("leak", values, "signature = ?", arrayOf(signature))
  }

  class LeakProjection(
    val shortDescription: String,
    val isNew: Boolean,
    val isLibraryLeak: Boolean,
    val leakTraces: List<LeakTraceProjection>
  )

  class LeakTraceProjection(
    val leakTraceIndex: Int,
    val heapAnalysisId: Long,
    val classSimpleName: String,
    val createdAtTimeMillis: Long
  )

  fun retrieveLeakBySignature(
    db: SQLiteDatabase,
    signature: String
  ): LeakProjection? {
    return db.rawQuery(
      """
          SELECT
          lt.leak_trace_index
          , lt.heap_analysis_id
          , lt.class_simple_name
          , h.created_at_time_millis
          , l.short_description
          , l.is_read
          , l.is_library_leak
          FROM leak_trace lt
          LEFT JOIN leak l on lt.leak_id = l.id
          LEFT JOIN heap_analysis h ON lt.heap_analysis_id = h.id
          WHERE l.signature = ?
          ORDER BY h.created_at_time_millis DESC
          """, arrayOf(signature)
    )
      .use { cursor ->
        return if (cursor.moveToFirst()) {
          val leakTraces = mutableListOf<LeakTraceProjection>()
          val leakProjection = LeakProjection(
            shortDescription = cursor.getString(4),
            isNew = cursor.getInt(5) == 0,
            isLibraryLeak = cursor.getInt(6) == 1,
            leakTraces = leakTraces
          )
          leakTraces.addAll(generateSequence(cursor) {
            if (cursor.moveToNext()) cursor else null
          }.map {
            LeakTraceProjection(
              leakTraceIndex = cursor.getInt(0),
              heapAnalysisId = cursor.getLong(1),
              classSimpleName = cursor.getString(2),
              createdAtTimeMillis = cursor.getLong(3)
            )
          })
          leakProjection
        } else {
          null
        }
      }
  }

  fun deleteByHeapAnalysisId(
    db: SQLiteDatabase,
    heapAnalysisId: Long
  ) {
    LeakTraceTable.deleteByHeapAnalysisId(db, heapAnalysisId)
    db.execSQL(
      """
      DELETE
      FROM leak
      WHERE NOT EXISTS (
      SELECT *
      FROM leak_trace lt
      WHERE leak.id = lt.leak_id)
    """
    )
  }
}