1 /* <lambda>null2 * Copyright (C) 2016 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.Manifest.permission.WRITE_EXTERNAL_STORAGE 19 import android.annotation.TargetApi 20 import android.content.Context 21 import android.content.pm.PackageManager.PERMISSION_GRANTED 22 import android.os.Build.VERSION.SDK_INT 23 import android.os.Build.VERSION_CODES.M 24 import android.os.Environment 25 import android.os.Environment.DIRECTORY_DOWNLOADS 26 import com.squareup.leakcanary.core.R 27 import leakcanary.internal.NotificationType.LEAKCANARY_LOW 28 import shark.SharkLog 29 import java.io.File 30 import java.io.FilenameFilter 31 import java.text.SimpleDateFormat 32 import java.util.ArrayList 33 import java.util.Date 34 import java.util.Locale 35 36 /** 37 * Provides access to where heap dumps and analysis results will be stored. 38 */ 39 internal class LeakDirectoryProvider constructor( 40 context: Context, 41 private val maxStoredHeapDumps: () -> Int, 42 private val requestExternalStoragePermission: () -> Boolean 43 ) { 44 private val context: Context = context.applicationContext 45 46 fun newHeapDumpFile(): File? { 47 cleanupOldHeapDumps() 48 49 var storageDirectory = externalStorageDirectory() 50 if (!directoryWritableAfterMkdirs(storageDirectory)) { 51 if (!hasStoragePermission()) { 52 if (requestExternalStoragePermission()) { 53 SharkLog.d { "WRITE_EXTERNAL_STORAGE permission not granted, requesting" } 54 requestWritePermissionNotification() 55 } else { 56 SharkLog.d { "WRITE_EXTERNAL_STORAGE permission not granted, ignoring" } 57 } 58 } else { 59 val state = Environment.getExternalStorageState() 60 if (Environment.MEDIA_MOUNTED != state) { 61 SharkLog.d { "External storage not mounted, state: $state" } 62 } else { 63 SharkLog.d { 64 "Could not create heap dump directory in external storage: [${storageDirectory.absolutePath}]" 65 } 66 } 67 } 68 // Fallback to app storage. 69 storageDirectory = appStorageDirectory() 70 if (!directoryWritableAfterMkdirs(storageDirectory)) { 71 SharkLog.d { 72 "Could not create heap dump directory in app storage: [${storageDirectory.absolutePath}]" 73 } 74 return null 75 } 76 } 77 78 val fileName = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss_SSS'.hprof'", Locale.US).format(Date()) 79 return File(storageDirectory, fileName) 80 } 81 82 @TargetApi(M) fun hasStoragePermission(): Boolean { 83 if (SDK_INT < M) { 84 return true 85 } 86 // Once true, this won't change for the life of the process so we can cache it. 87 if (writeExternalStorageGranted) { 88 return true 89 } 90 writeExternalStorageGranted = 91 context.checkSelfPermission(WRITE_EXTERNAL_STORAGE) == PERMISSION_GRANTED 92 return writeExternalStorageGranted 93 } 94 95 fun requestWritePermissionNotification() { 96 if (permissionNotificationDisplayed || !Notifications.canShowNotification) { 97 return 98 } 99 permissionNotificationDisplayed = true 100 101 val pendingIntent = 102 RequestPermissionActivity.createPendingIntent(context, WRITE_EXTERNAL_STORAGE) 103 val contentTitle = context.getString( 104 R.string.leak_canary_permission_notification_title 105 ) 106 val packageName = context.packageName 107 val contentText = 108 context.getString(R.string.leak_canary_permission_notification_text, packageName) 109 110 Notifications.showNotification( 111 context, contentTitle, contentText, pendingIntent, 112 R.id.leak_canary_notification_write_permission, LEAKCANARY_LOW 113 ) 114 } 115 116 @Suppress("DEPRECATION") 117 private fun externalStorageDirectory(): File { 118 val downloadsDirectory = Environment.getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS) 119 return File(downloadsDirectory, "leakcanary-" + context.packageName) 120 } 121 122 private fun appStorageDirectory(): File { 123 val appFilesDirectory = context.cacheDir 124 return File(appFilesDirectory, "leakcanary") 125 } 126 127 private fun directoryWritableAfterMkdirs(directory: File): Boolean { 128 val success = directory.mkdirs() 129 return (success || directory.exists()) && directory.canWrite() 130 } 131 132 private fun cleanupOldHeapDumps() { 133 val hprofFiles = listWritableFiles { _, name -> 134 name.endsWith( 135 HPROF_SUFFIX 136 ) 137 } 138 val maxStoredHeapDumps = maxStoredHeapDumps() 139 if (maxStoredHeapDumps < 1) { 140 throw IllegalArgumentException("maxStoredHeapDumps must be at least 1") 141 } 142 143 val filesToRemove = hprofFiles.size - maxStoredHeapDumps 144 if (filesToRemove > 0) { 145 SharkLog.d { "Removing $filesToRemove heap dumps" } 146 // Sort with oldest modified first. 147 hprofFiles.sortWith { lhs, rhs -> 148 java.lang.Long.valueOf(lhs.lastModified()) 149 .compareTo(rhs.lastModified()) 150 } 151 for (i in 0 until filesToRemove) { 152 val path = hprofFiles[i].absolutePath 153 val deleted = hprofFiles[i].delete() 154 if (deleted) { 155 filesDeletedTooOld += path 156 } else { 157 SharkLog.d { "Could not delete old hprof file ${hprofFiles[i].path}" } 158 } 159 } 160 } 161 } 162 163 private fun listWritableFiles(filter: FilenameFilter): MutableList<File> { 164 val files = ArrayList<File>() 165 166 val externalStorageDirectory = externalStorageDirectory() 167 if (externalStorageDirectory.exists() && externalStorageDirectory.canWrite()) { 168 val externalFiles = externalStorageDirectory.listFiles(filter) 169 if (externalFiles != null) { 170 files.addAll(externalFiles) 171 } 172 } 173 174 val appFiles = appStorageDirectory().listFiles(filter) 175 if (appFiles != null) { 176 files.addAll(appFiles) 177 } 178 return files 179 } 180 181 companion object { 182 @Volatile private var writeExternalStorageGranted: Boolean = false 183 @Volatile private var permissionNotificationDisplayed: Boolean = false 184 185 private val filesDeletedTooOld = mutableListOf<String>() 186 val filesDeletedRemoveLeak = mutableListOf<String>() 187 188 private const val HPROF_SUFFIX = ".hprof" 189 190 fun hprofDeleteReason(file: File): String { 191 val path = file.absolutePath 192 return when { 193 filesDeletedTooOld.contains(path) -> "older than all other hprof files" 194 filesDeletedRemoveLeak.contains(path) -> "leak manually removed" 195 else -> "unknown" 196 } 197 } 198 } 199 } 200