<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