main

square/leakcanary

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

LeakDirectoryProvider.kt

TLDR

The file LeakDirectoryProvider.kt is part of the LeakCanary library and provides access to where heap dumps and analysis results will be stored.

Methods

newHeapDumpFile

This method creates a new heap dump file in the storage directory. It checks if the external storage is available and if the app has the necessary permissions. If the external storage is not available or the app does not have the permission to write to external storage, it falls back to the app storage directory. The method returns the created heap dump file.

hasStoragePermission

This method checks if the app has the permission to write to external storage. It returns true if the permission is granted, or if the device SDK version is lower than M (Android Marshmallow).

requestWritePermissionNotification

This method shows a notification requesting the WRITE_EXTERNAL_STORAGE permission from the user. The notification is only shown once and depends on the availability of showing notifications.

cleanupOldHeapDumps

This method cleans up old heap dump files based on the maximum number of allowed stored heap dumps. It lists all the writable heap dump files, sorts them by the last modified time, and deletes the extra files if necessary.

listWritableFiles

This method lists all the writable files in the storage directory based on the given filename filter. It combines the files from the external storage directory and the app storage directory, and returns a list of the matching files.

Classes

None

/*
 * Copyright (C) 2016 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package leakcanary.internal

import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.annotation.TargetApi
import android.content.Context
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.M
import android.os.Environment
import android.os.Environment.DIRECTORY_DOWNLOADS
import com.squareup.leakcanary.core.R
import leakcanary.internal.NotificationType.LEAKCANARY_LOW
import shark.SharkLog
import java.io.File
import java.io.FilenameFilter
import java.text.SimpleDateFormat
import java.util.ArrayList
import java.util.Date
import java.util.Locale

/**
 * Provides access to where heap dumps and analysis results will be stored.
 */
internal class LeakDirectoryProvider constructor(
  context: Context,
  private val maxStoredHeapDumps: () -> Int,
  private val requestExternalStoragePermission: () -> Boolean
) {
  private val context: Context = context.applicationContext

  fun newHeapDumpFile(): File? {
    cleanupOldHeapDumps()

    var storageDirectory = externalStorageDirectory()
    if (!directoryWritableAfterMkdirs(storageDirectory)) {
      if (!hasStoragePermission()) {
        if (requestExternalStoragePermission()) {
          SharkLog.d { "WRITE_EXTERNAL_STORAGE permission not granted, requesting" }
          requestWritePermissionNotification()
        } else {
          SharkLog.d { "WRITE_EXTERNAL_STORAGE permission not granted, ignoring" }
        }
      } else {
        val state = Environment.getExternalStorageState()
        if (Environment.MEDIA_MOUNTED != state) {
          SharkLog.d { "External storage not mounted, state: $state" }
        } else {
          SharkLog.d {
            "Could not create heap dump directory in external storage: [${storageDirectory.absolutePath}]"
          }
        }
      }
      // Fallback to app storage.
      storageDirectory = appStorageDirectory()
      if (!directoryWritableAfterMkdirs(storageDirectory)) {
        SharkLog.d {
          "Could not create heap dump directory in app storage: [${storageDirectory.absolutePath}]"
        }
        return null
      }
    }

    val fileName = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss_SSS'.hprof'", Locale.US).format(Date())
    return File(storageDirectory, fileName)
  }

  @TargetApi(M) fun hasStoragePermission(): Boolean {
    if (SDK_INT < M) {
      return true
    }
    // Once true, this won't change for the life of the process so we can cache it.
    if (writeExternalStorageGranted) {
      return true
    }
    writeExternalStorageGranted =
      context.checkSelfPermission(WRITE_EXTERNAL_STORAGE) == PERMISSION_GRANTED
    return writeExternalStorageGranted
  }

  fun requestWritePermissionNotification() {
    if (permissionNotificationDisplayed || !Notifications.canShowNotification) {
      return
    }
    permissionNotificationDisplayed = true

    val pendingIntent =
      RequestPermissionActivity.createPendingIntent(context, WRITE_EXTERNAL_STORAGE)
    val contentTitle = context.getString(
      R.string.leak_canary_permission_notification_title
    )
    val packageName = context.packageName
    val contentText =
      context.getString(R.string.leak_canary_permission_notification_text, packageName)

    Notifications.showNotification(
      context, contentTitle, contentText, pendingIntent,
      R.id.leak_canary_notification_write_permission, LEAKCANARY_LOW
    )
  }

  @Suppress("DEPRECATION")
  private fun externalStorageDirectory(): File {
    val downloadsDirectory = Environment.getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS)
    return File(downloadsDirectory, "leakcanary-" + context.packageName)
  }

  private fun appStorageDirectory(): File {
    val appFilesDirectory = context.cacheDir
    return File(appFilesDirectory, "leakcanary")
  }

  private fun directoryWritableAfterMkdirs(directory: File): Boolean {
    val success = directory.mkdirs()
    return (success || directory.exists()) && directory.canWrite()
  }

  private fun cleanupOldHeapDumps() {
    val hprofFiles = listWritableFiles { _, name ->
      name.endsWith(
        HPROF_SUFFIX
      )
    }
    val maxStoredHeapDumps = maxStoredHeapDumps()
    if (maxStoredHeapDumps < 1) {
      throw IllegalArgumentException("maxStoredHeapDumps must be at least 1")
    }

    val filesToRemove = hprofFiles.size - maxStoredHeapDumps
    if (filesToRemove > 0) {
      SharkLog.d { "Removing $filesToRemove heap dumps" }
      // Sort with oldest modified first.
      hprofFiles.sortWith { lhs, rhs ->
        java.lang.Long.valueOf(lhs.lastModified())
          .compareTo(rhs.lastModified())
      }
      for (i in 0 until filesToRemove) {
        val path = hprofFiles[i].absolutePath
        val deleted = hprofFiles[i].delete()
        if (deleted) {
          filesDeletedTooOld += path
        } else {
          SharkLog.d { "Could not delete old hprof file ${hprofFiles[i].path}" }
        }
      }
    }
  }

  private fun listWritableFiles(filter: FilenameFilter): MutableList<File> {
    val files = ArrayList<File>()

    val externalStorageDirectory = externalStorageDirectory()
    if (externalStorageDirectory.exists() && externalStorageDirectory.canWrite()) {
      val externalFiles = externalStorageDirectory.listFiles(filter)
      if (externalFiles != null) {
        files.addAll(externalFiles)
      }
    }

    val appFiles = appStorageDirectory().listFiles(filter)
    if (appFiles != null) {
      files.addAll(appFiles)
    }
    return files
  }

  companion object {
    @Volatile private var writeExternalStorageGranted: Boolean = false
    @Volatile private var permissionNotificationDisplayed: Boolean = false

    private val filesDeletedTooOld = mutableListOf<String>()
    val filesDeletedRemoveLeak = mutableListOf<String>()

    private const val HPROF_SUFFIX = ".hprof"

    fun hprofDeleteReason(file: File): String {
      val path = file.absolutePath
      return when {
        filesDeletedTooOld.contains(path) -> "older than all other hprof files"
        filesDeletedRemoveLeak.contains(path) -> "leak manually removed"
        else -> "unknown"
      }
    }
  }
}