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