<lambda>null1 package leakcanary.internal
2
3 import android.app.Application
4 import android.app.Notification
5 import android.app.NotificationManager
6 import android.content.Context
7 import android.content.res.Resources.NotFoundException
8 import android.os.Handler
9 import android.os.SystemClock
10 import com.squareup.leakcanary.core.R
11 import java.util.UUID
12 import leakcanary.AppWatcher
13 import leakcanary.EventListener.Event.DumpingHeap
14 import leakcanary.EventListener.Event.HeapDump
15 import leakcanary.EventListener.Event.HeapDumpFailed
16 import leakcanary.GcTrigger
17 import leakcanary.KeyedWeakReference
18 import leakcanary.LeakCanary.Config
19 import leakcanary.ObjectWatcher
20 import leakcanary.internal.HeapDumpControl.ICanHazHeap.Nope
21 import leakcanary.internal.HeapDumpControl.ICanHazHeap.NotifyingNope
22 import leakcanary.internal.InternalLeakCanary.onRetainInstanceListener
23 import leakcanary.internal.NotificationReceiver.Action.CANCEL_NOTIFICATION
24 import leakcanary.internal.NotificationReceiver.Action.DUMP_HEAP
25 import leakcanary.internal.NotificationType.LEAKCANARY_LOW
26 import leakcanary.internal.RetainInstanceEvent.CountChanged.BelowThreshold
27 import leakcanary.internal.RetainInstanceEvent.CountChanged.DumpHappenedRecently
28 import leakcanary.internal.RetainInstanceEvent.CountChanged.DumpingDisabled
29 import leakcanary.internal.RetainInstanceEvent.NoMoreObjects
30 import leakcanary.internal.friendly.measureDurationMillis
31 import shark.AndroidResourceIdNames
32 import shark.SharkLog
33
34 internal class HeapDumpTrigger(
35 private val application: Application,
36 private val backgroundHandler: Handler,
37 private val objectWatcher: ObjectWatcher,
38 private val gcTrigger: GcTrigger,
39 private val configProvider: () -> Config
40 ) {
41
42 private val notificationManager
43 get() =
44 application.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
45
46 private val applicationVisible
47 get() = applicationInvisibleAt == -1L
48
49 @Volatile
50 private var checkScheduledAt: Long = 0L
51
52 private var lastDisplayedRetainedObjectCount = 0
53
54 private var lastHeapDumpUptimeMillis = 0L
55
56 private val scheduleDismissRetainedCountNotification = {
57 dismissRetainedCountNotification()
58 }
59
60 private val scheduleDismissNoRetainedOnTapNotification = {
61 dismissNoRetainedOnTapNotification()
62 }
63
64 /**
65 * When the app becomes invisible, we don't dump the heap immediately. Instead we wait in case
66 * the app came back to the foreground, but also to wait for new leaks that typically occur on
67 * back press (activity destroy).
68 */
69 private val applicationInvisibleLessThanWatchPeriod: Boolean
70 get() {
71 val applicationInvisibleAt = applicationInvisibleAt
72 return applicationInvisibleAt != -1L && SystemClock.uptimeMillis() - applicationInvisibleAt < AppWatcher.retainedDelayMillis
73 }
74
75 @Volatile
76 private var applicationInvisibleAt = -1L
77
78 // Needs to be lazy because on Android 16, UUID.randomUUID().toString() will trigger a disk read
79 // violation by calling RandomBitsSupplier.getUnixDeviceRandom()
80 // Can't be lazy because this is a var.
81 private var currentEventUniqueId: String? = null
82
83 fun onApplicationVisibilityChanged(applicationVisible: Boolean) {
84 if (applicationVisible) {
85 applicationInvisibleAt = -1L
86 } else {
87 applicationInvisibleAt = SystemClock.uptimeMillis()
88 // Scheduling for after watchDuration so that any destroyed activity has time to become
89 // watch and be part of this analysis.
90 scheduleRetainedObjectCheck(
91 delayMillis = AppWatcher.retainedDelayMillis
92 )
93 }
94 }
95
96 private fun checkRetainedObjects() {
97 val iCanHasHeap = HeapDumpControl.iCanHasHeap()
98
99 val config = configProvider()
100
101 if (iCanHasHeap is Nope) {
102 if (iCanHasHeap is NotifyingNope) {
103 // Before notifying that we can't dump heap, let's check if we still have retained object.
104 var retainedReferenceCount = objectWatcher.retainedObjectCount
105
106 if (retainedReferenceCount > 0) {
107 gcTrigger.runGc()
108 retainedReferenceCount = objectWatcher.retainedObjectCount
109 }
110
111 val nopeReason = iCanHasHeap.reason()
112 val wouldDump = !checkRetainedCount(
113 retainedReferenceCount, config.retainedVisibleThreshold, nopeReason
114 )
115
116 if (wouldDump) {
117 val uppercaseReason = nopeReason[0].toUpperCase() + nopeReason.substring(1)
118 onRetainInstanceListener.onEvent(DumpingDisabled(uppercaseReason))
119 showRetainedCountNotification(
120 objectCount = retainedReferenceCount,
121 contentText = uppercaseReason
122 )
123 }
124 } else {
125 SharkLog.d {
126 application.getString(
127 R.string.leak_canary_heap_dump_disabled_text, iCanHasHeap.reason()
128 )
129 }
130 }
131 return
132 }
133
134 var retainedReferenceCount = objectWatcher.retainedObjectCount
135
136 if (retainedReferenceCount > 0) {
137 gcTrigger.runGc()
138 retainedReferenceCount = objectWatcher.retainedObjectCount
139 }
140
141 if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return
142
143 val now = SystemClock.uptimeMillis()
144 val elapsedSinceLastDumpMillis = now - lastHeapDumpUptimeMillis
145 if (elapsedSinceLastDumpMillis < WAIT_BETWEEN_HEAP_DUMPS_MILLIS) {
146 onRetainInstanceListener.onEvent(DumpHappenedRecently)
147 showRetainedCountNotification(
148 objectCount = retainedReferenceCount,
149 contentText = application.getString(R.string.leak_canary_notification_retained_dump_wait)
150 )
151 scheduleRetainedObjectCheck(
152 delayMillis = WAIT_BETWEEN_HEAP_DUMPS_MILLIS - elapsedSinceLastDumpMillis
153 )
154 return
155 }
156
157 dismissRetainedCountNotification()
158 val visibility = if (applicationVisible) "visible" else "not visible"
159 dumpHeap(
160 retainedReferenceCount = retainedReferenceCount,
161 retry = true,
162 reason = "$retainedReferenceCount retained objects, app is $visibility"
163 )
164 }
165
166 private fun dumpHeap(
167 retainedReferenceCount: Int,
168 retry: Boolean,
169 reason: String
170 ) {
171 val directoryProvider =
172 InternalLeakCanary.createLeakDirectoryProvider(InternalLeakCanary.application)
173 val heapDumpFile = directoryProvider.newHeapDumpFile()
174
175 val durationMillis: Long
176 if (currentEventUniqueId == null) {
177 currentEventUniqueId = UUID.randomUUID().toString()
178 }
179 try {
180 InternalLeakCanary.sendEvent(DumpingHeap(currentEventUniqueId!!))
181 if (heapDumpFile == null) {
182 throw RuntimeException("Could not create heap dump file")
183 }
184 saveResourceIdNamesToMemory()
185 val heapDumpUptimeMillis = SystemClock.uptimeMillis()
186 KeyedWeakReference.heapDumpUptimeMillis = heapDumpUptimeMillis
187 durationMillis = measureDurationMillis {
188 configProvider().heapDumper.dumpHeap(heapDumpFile)
189 }
190 if (heapDumpFile.length() == 0L) {
191 throw RuntimeException("Dumped heap file is 0 byte length")
192 }
193 lastDisplayedRetainedObjectCount = 0
194 lastHeapDumpUptimeMillis = SystemClock.uptimeMillis()
195 objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis)
196 currentEventUniqueId = UUID.randomUUID().toString()
197 InternalLeakCanary.sendEvent(HeapDump(currentEventUniqueId!!, heapDumpFile, durationMillis, reason))
198 } catch (throwable: Throwable) {
199 InternalLeakCanary.sendEvent(HeapDumpFailed(currentEventUniqueId!!, throwable, retry))
200 if (retry) {
201 scheduleRetainedObjectCheck(
202 delayMillis = WAIT_AFTER_DUMP_FAILED_MILLIS
203 )
204 }
205 showRetainedCountNotification(
206 objectCount = retainedReferenceCount,
207 contentText = application.getString(
208 R.string.leak_canary_notification_retained_dump_failed
209 )
210 )
211 return
212 }
213 }
214
215 /**
216 * Stores in memory the mapping of resource id ints to their corresponding name, so that the heap
217 * analysis can label views with their resource id names.
218 */
219 private fun saveResourceIdNamesToMemory() {
220 val resources = application.resources
221 AndroidResourceIdNames.saveToMemory(
222 getResourceTypeName = { id ->
223 try {
224 resources.getResourceTypeName(id)
225 } catch (e: NotFoundException) {
226 null
227 }
228 },
229 getResourceEntryName = { id ->
230 try {
231 resources.getResourceEntryName(id)
232 } catch (e: NotFoundException) {
233 null
234 }
235 })
236 }
237
238 fun onDumpHeapReceived(forceDump: Boolean) {
239 backgroundHandler.post {
240 dismissNoRetainedOnTapNotification()
241 gcTrigger.runGc()
242 val retainedReferenceCount = objectWatcher.retainedObjectCount
243 if (!forceDump && retainedReferenceCount == 0) {
244 SharkLog.d { "Ignoring user request to dump heap: no retained objects remaining after GC" }
245 @Suppress("DEPRECATION")
246 val builder = Notification.Builder(application)
247 .setContentTitle(
248 application.getString(R.string.leak_canary_notification_no_retained_object_title)
249 )
250 .setContentText(
251 application.getString(
252 R.string.leak_canary_notification_no_retained_object_content
253 )
254 )
255 .setAutoCancel(true)
256 .setContentIntent(NotificationReceiver.pendingIntent(application, CANCEL_NOTIFICATION))
257 val notification =
258 Notifications.buildNotification(application, builder, LEAKCANARY_LOW)
259 notificationManager.notify(
260 R.id.leak_canary_notification_no_retained_object_on_tap, notification
261 )
262 backgroundHandler.postDelayed(
263 scheduleDismissNoRetainedOnTapNotification,
264 DISMISS_NO_RETAINED_OBJECT_NOTIFICATION_MILLIS
265 )
266 lastDisplayedRetainedObjectCount = 0
267 return@post
268 }
269
270 SharkLog.d { "Dumping the heap because user requested it" }
271 dumpHeap(retainedReferenceCount, retry = false, "user request")
272 }
273 }
274
275 private fun checkRetainedCount(
276 retainedKeysCount: Int,
277 retainedVisibleThreshold: Int,
278 nopeReason: String? = null
279 ): Boolean {
280 val countChanged = lastDisplayedRetainedObjectCount != retainedKeysCount
281 lastDisplayedRetainedObjectCount = retainedKeysCount
282 if (retainedKeysCount == 0) {
283 if (countChanged) {
284 SharkLog.d { "All retained objects have been garbage collected" }
285 onRetainInstanceListener.onEvent(NoMoreObjects)
286 showNoMoreRetainedObjectNotification()
287 }
288 return true
289 }
290
291 val applicationVisible = applicationVisible
292 val applicationInvisibleLessThanWatchPeriod = applicationInvisibleLessThanWatchPeriod
293
294 if (countChanged) {
295 val whatsNext = if (applicationVisible) {
296 if (retainedKeysCount < retainedVisibleThreshold) {
297 "not dumping heap yet (app is visible & < $retainedVisibleThreshold threshold)"
298 } else {
299 if (nopeReason != null) {
300 "would dump heap now (app is visible & >=$retainedVisibleThreshold threshold) but $nopeReason"
301 } else {
302 "dumping heap now (app is visible & >=$retainedVisibleThreshold threshold)"
303 }
304 }
305 } else if (applicationInvisibleLessThanWatchPeriod) {
306 val wait =
307 AppWatcher.retainedDelayMillis - (SystemClock.uptimeMillis() - applicationInvisibleAt)
308 if (nopeReason != null) {
309 "would dump heap in $wait ms (app just became invisible) but $nopeReason"
310 } else {
311 "dumping heap in $wait ms (app just became invisible)"
312 }
313 } else {
314 if (nopeReason != null) {
315 "would dump heap now (app is invisible) but $nopeReason"
316 } else {
317 "dumping heap now (app is invisible)"
318 }
319 }
320
321 SharkLog.d {
322 val s = if (retainedKeysCount > 1) "s" else ""
323 "Found $retainedKeysCount object$s retained, $whatsNext"
324 }
325 }
326
327 if (retainedKeysCount < retainedVisibleThreshold) {
328 if (applicationVisible || applicationInvisibleLessThanWatchPeriod) {
329 if (countChanged) {
330 onRetainInstanceListener.onEvent(BelowThreshold(retainedKeysCount))
331 }
332 showRetainedCountNotification(
333 objectCount = retainedKeysCount,
334 contentText = application.getString(
335 R.string.leak_canary_notification_retained_visible, retainedVisibleThreshold
336 )
337 )
338 scheduleRetainedObjectCheck(
339 delayMillis = WAIT_FOR_OBJECT_THRESHOLD_MILLIS
340 )
341 return true
342 }
343 }
344 return false
345 }
346
347 fun scheduleRetainedObjectCheck(
348 delayMillis: Long = 0L
349 ) {
350 val checkCurrentlyScheduledAt = checkScheduledAt
351 if (checkCurrentlyScheduledAt > 0) {
352 return
353 }
354 checkScheduledAt = SystemClock.uptimeMillis() + delayMillis
355 backgroundHandler.postDelayed({
356 checkScheduledAt = 0
357 checkRetainedObjects()
358 }, delayMillis)
359 }
360
361 private fun showNoMoreRetainedObjectNotification() {
362 backgroundHandler.removeCallbacks(scheduleDismissRetainedCountNotification)
363 if (!Notifications.canShowNotification) {
364 return
365 }
366 val builder = Notification.Builder(application)
367 .setContentTitle(
368 application.getString(R.string.leak_canary_notification_no_retained_object_title)
369 )
370 .setContentText(
371 application.getString(
372 R.string.leak_canary_notification_no_retained_object_content
373 )
374 )
375 .setAutoCancel(true)
376 .setContentIntent(NotificationReceiver.pendingIntent(application, CANCEL_NOTIFICATION))
377 val notification =
378 Notifications.buildNotification(application, builder, LEAKCANARY_LOW)
379 notificationManager.notify(R.id.leak_canary_notification_retained_objects, notification)
380 backgroundHandler.postDelayed(
381 scheduleDismissRetainedCountNotification, DISMISS_NO_RETAINED_OBJECT_NOTIFICATION_MILLIS
382 )
383 }
384
385 private fun showRetainedCountNotification(
386 objectCount: Int,
387 contentText: String
388 ) {
389 backgroundHandler.removeCallbacks(scheduleDismissRetainedCountNotification)
390 if (!Notifications.canShowNotification) {
391 return
392 }
393 @Suppress("DEPRECATION")
394 val builder = Notification.Builder(application)
395 .setContentTitle(
396 application.getString(R.string.leak_canary_notification_retained_title, objectCount)
397 )
398 .setContentText(contentText)
399 .setAutoCancel(true)
400 .setContentIntent(NotificationReceiver.pendingIntent(application, DUMP_HEAP))
401 val notification =
402 Notifications.buildNotification(application, builder, LEAKCANARY_LOW)
403 notificationManager.notify(R.id.leak_canary_notification_retained_objects, notification)
404 }
405
406 private fun dismissRetainedCountNotification() {
407 backgroundHandler.removeCallbacks(scheduleDismissRetainedCountNotification)
408 notificationManager.cancel(R.id.leak_canary_notification_retained_objects)
409 }
410
411 private fun dismissNoRetainedOnTapNotification() {
412 backgroundHandler.removeCallbacks(scheduleDismissNoRetainedOnTapNotification)
413 notificationManager.cancel(R.id.leak_canary_notification_no_retained_object_on_tap)
414 }
415
416 companion object {
417 internal const val WAIT_AFTER_DUMP_FAILED_MILLIS = 5_000L
418 private const val WAIT_FOR_OBJECT_THRESHOLD_MILLIS = 2_000L
419 private const val DISMISS_NO_RETAINED_OBJECT_NOTIFICATION_MILLIS = 30_000L
420 private const val WAIT_BETWEEN_HEAP_DUMPS_MILLIS = 60_000L
421 }
422 }
423