Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move Notifications to get triggered from NotificationEventListener #2395

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 14 additions & 0 deletions leakcanary-android-core/api/leakcanary-android-core.api
Expand Up @@ -36,6 +36,10 @@ public abstract class leakcanary/EventListener$Event : java/io/Serializable {
public final fun getUniqueId ()Ljava/lang/String;
}

public final class leakcanary/EventListener$Event$DismissNoRetainedOnTapNotification : leakcanary/EventListener$Event {
public fun <init> (Ljava/lang/String;)V
}

public final class leakcanary/EventListener$Event$DumpingHeap : leakcanary/EventListener$Event {
public fun <init> (Ljava/lang/String;)V
}
Expand Down Expand Up @@ -74,6 +78,16 @@ public final class leakcanary/EventListener$Event$HeapDumpFailed : leakcanary/Ev
public final fun getWillRetryLater ()Z
}

public final class leakcanary/EventListener$Event$ShowNoMoreRetainedObjectFoundNotification : leakcanary/EventListener$Event {
public fun <init> (Ljava/lang/String;)V
}

public final class leakcanary/EventListener$Event$ShowRetainedCountNotification : leakcanary/EventListener$Event {
public fun <init> (Ljava/lang/String;ILjava/lang/String;)V
public final fun getContentText ()Ljava/lang/String;
public final fun getObjectCount ()I
}

public abstract interface class leakcanary/HeapDumper {
public abstract fun dumpHeap (Ljava/io/File;)V
}
Expand Down
14 changes: 14 additions & 0 deletions leakcanary-android-core/src/main/java/leakcanary/EventListener.kt
Expand Up @@ -86,6 +86,20 @@ fun interface EventListener {
showIntent: Intent
) : HeapAnalysisDone<HeapAnalysisFailure>(uniqueId, heapAnalysis, showIntent)
}

class DismissNoRetainedOnTapNotification(
uniqueId: String,
) : Event(uniqueId)

class ShowNoMoreRetainedObjectFoundNotification(
uniqueId: String,
) : Event(uniqueId)

class ShowRetainedCountNotification(
uniqueId: String,
val objectCount: Int,
val contentText: String
) : Event(uniqueId)
}

/**
Expand Down
Expand Up @@ -11,18 +11,28 @@ import leakcanary.EventListener.Event.DumpingHeap
import leakcanary.EventListener.Event.HeapAnalysisDone
import leakcanary.EventListener.Event.HeapAnalysisDone.HeapAnalysisSucceeded
import leakcanary.EventListener.Event.HeapAnalysisProgress
import leakcanary.EventListener.Event.HeapDumpFailed
import leakcanary.EventListener.Event.HeapDump
import leakcanary.EventListener.Event.HeapDumpFailed
import leakcanary.internal.InternalLeakCanary
import leakcanary.internal.NotificationReceiver
import leakcanary.internal.NotificationType.LEAKCANARY_LOW
import leakcanary.internal.NotificationType.LEAKCANARY_MAX
import leakcanary.internal.Notifications
import leakcanary.internal.friendly.mainHandler

private const val DISMISS_NO_RETAINED_OBJECT_NOTIFICATION_MILLIS = 30_000L

object NotificationEventListener : EventListener {

private val appContext = InternalLeakCanary.application
private val notificationManager =
appContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
private val scheduleDismissRetainedCountNotification = {
dismissRetainedCountNotification()
}
private val scheduleDismissNoRetainedOnTapNotification = {
dismissNoRetainedOnTapNotification()
}

override fun onEvent(event: Event) {
// TODO Unify Notifications.buildNotification vs Notifications.showNotification
Expand Down Expand Up @@ -72,12 +82,80 @@ object NotificationEventListener : EventListener {
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
val pendingIntent = PendingIntent.getActivity(appContext, 1, event.showIntent, flags)
showHeapAnalysisResultNotification(contentTitle,pendingIntent)
val pendingIntent = PendingIntent.getActivity(appContext, 1, event.showIntent, flags)
showHeapAnalysisResultNotification(contentTitle, pendingIntent)
}

is Event.DismissNoRetainedOnTapNotification -> {
dismissNoRetainedOnTapNotification()
}

is Event.ShowNoMoreRetainedObjectFoundNotification -> {
mainHandler.removeCallbacks(scheduleDismissNoRetainedOnTapNotification)
sendShowNoMoreRetainedObjectFoundNotification()

mainHandler.postDelayed(
scheduleDismissNoRetainedOnTapNotification,
DISMISS_NO_RETAINED_OBJECT_NOTIFICATION_MILLIS
)
}

is Event.ShowRetainedCountNotification -> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally the names of the events wouldn't have "Notification" in them, and instead describe "what just happened". Then the notification listener can turn those events into the proper notifications.

mainHandler.removeCallbacks(scheduleDismissRetainedCountNotification)
sendShowRetainedCountNotification(event.objectCount, event.contentText)
}
}
}

private fun sendShowNoMoreRetainedObjectFoundNotification() {
@Suppress("DEPRECATION")
val builder = Notification.Builder(appContext)
.setContentTitle(
appContext.getString(R.string.leak_canary_notification_no_retained_object_title)
)
.setContentText(
appContext.getString(
R.string.leak_canary_notification_no_retained_object_content
)
)
.setAutoCancel(true)
.setContentIntent(
NotificationReceiver.pendingIntent(
appContext,
NotificationReceiver.Action.CANCEL_NOTIFICATION
)
)
val notification =
Notifications.buildNotification(appContext, builder, LEAKCANARY_LOW)
notificationManager.notify(
R.id.leak_canary_notification_no_retained_object_on_tap, notification
)
}

private fun sendShowRetainedCountNotification(objectCount: Int, contentText: String) {
@Suppress("DEPRECATION")
val builder = Notification.Builder(appContext)
.setContentTitle(
appContext.getString(R.string.leak_canary_notification_retained_title, objectCount)
)
.setContentText(contentText)
.setAutoCancel(true)
.setContentIntent(NotificationReceiver.pendingIntent(appContext, NotificationReceiver.Action.DUMP_HEAP))
val notification =
Notifications.buildNotification(appContext, builder, LEAKCANARY_LOW)
notificationManager.notify(R.id.leak_canary_notification_retained_objects, notification)
}

private fun dismissNoRetainedOnTapNotification() {
mainHandler.removeCallbacks(scheduleDismissNoRetainedOnTapNotification)
notificationManager.cancel(R.id.leak_canary_notification_no_retained_object_on_tap)
}

private fun dismissRetainedCountNotification() {
mainHandler.removeCallbacks(scheduleDismissRetainedCountNotification)
notificationManager.cancel(R.id.leak_canary_notification_retained_objects)
}

private fun showHeapAnalysisResultNotification(contentTitle: String, showIntent: PendingIntent) {
val contentText = appContext.getString(R.string.leak_canary_notification_message)
Notifications.showNotification(
Expand Down
@@ -1,28 +1,25 @@
package leakcanary.internal

import android.app.Application
import android.app.Notification
import android.app.NotificationManager
import android.content.Context
import android.content.res.Resources.NotFoundException
import android.os.Handler
import android.os.SystemClock
import com.squareup.leakcanary.core.R
import java.util.UUID
import leakcanary.AppWatcher
import leakcanary.EventListener
import leakcanary.EventListener.Event.DismissNoRetainedOnTapNotification
import leakcanary.EventListener.Event.DumpingHeap
import leakcanary.EventListener.Event.HeapDump
import leakcanary.EventListener.Event.HeapDumpFailed
import leakcanary.EventListener.Event.ShowNoMoreRetainedObjectFoundNotification
import leakcanary.GcTrigger
import leakcanary.KeyedWeakReference
import leakcanary.LeakCanary.Config
import leakcanary.ObjectWatcher
import leakcanary.internal.HeapDumpControl.ICanHazHeap.Nope
import leakcanary.internal.HeapDumpControl.ICanHazHeap.NotifyingNope
import leakcanary.internal.InternalLeakCanary.onRetainInstanceListener
import leakcanary.internal.NotificationReceiver.Action.CANCEL_NOTIFICATION
import leakcanary.internal.NotificationReceiver.Action.DUMP_HEAP
import leakcanary.internal.NotificationType.LEAKCANARY_LOW
import leakcanary.internal.RetainInstanceEvent.CountChanged.BelowThreshold
import leakcanary.internal.RetainInstanceEvent.CountChanged.DumpHappenedRecently
import leakcanary.internal.RetainInstanceEvent.CountChanged.DumpingDisabled
Expand All @@ -39,10 +36,6 @@ internal class HeapDumpTrigger(
private val configProvider: () -> Config
) {

private val notificationManager
get() =
application.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

private val applicationVisible
get() = applicationInvisibleAt == -1L

Expand All @@ -53,14 +46,6 @@ internal class HeapDumpTrigger(

private var lastHeapDumpUptimeMillis = 0L

private val scheduleDismissRetainedCountNotification = {
dismissRetainedCountNotification()
}

private val scheduleDismissNoRetainedOnTapNotification = {
dismissNoRetainedOnTapNotification()
}

/**
* When the app becomes invisible, we don't dump the heap immediately. Instead we wait in case
* the app came back to the foreground, but also to wait for new leaks that typically occur on
Expand All @@ -78,7 +63,9 @@ internal class HeapDumpTrigger(
// Needs to be lazy because on Android 16, UUID.randomUUID().toString() will trigger a disk read
// violation by calling RandomBitsSupplier.getUnixDeviceRandom()
// Can't be lazy because this is a var.
private var currentEventUniqueId: String? = null
private val currentEventUniqueId: String by lazy {
UUID.randomUUID().toString()
}

fun onApplicationVisibilityChanged(applicationVisible: Boolean) {
if (applicationVisible) {
Expand Down Expand Up @@ -154,7 +141,6 @@ internal class HeapDumpTrigger(
return
}

dismissRetainedCountNotification()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this has been replaced by new behavior in the event listener class, as I'm seeing no direct call to dismissRetainedCountNotification()

val visibility = if (applicationVisible) "visible" else "not visible"
dumpHeap(
retainedReferenceCount = retainedReferenceCount,
Expand All @@ -173,11 +159,8 @@ internal class HeapDumpTrigger(
val heapDumpFile = directoryProvider.newHeapDumpFile()

val durationMillis: Long
if (currentEventUniqueId == null) {
currentEventUniqueId = UUID.randomUUID().toString()
}
try {
InternalLeakCanary.sendEvent(DumpingHeap(currentEventUniqueId!!))
InternalLeakCanary.sendEvent(DumpingHeap(currentEventUniqueId))
if (heapDumpFile == null) {
throw RuntimeException("Could not create heap dump file")
}
Expand All @@ -193,10 +176,9 @@ internal class HeapDumpTrigger(
lastDisplayedRetainedObjectCount = 0
lastHeapDumpUptimeMillis = SystemClock.uptimeMillis()
objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis)
currentEventUniqueId = UUID.randomUUID().toString()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

currentEventUniqueId was previously changing on every actual dump. Looks like that's not the case anymore? So all dump heap events get the same id? That doesn't seem right?

InternalLeakCanary.sendEvent(HeapDump(currentEventUniqueId!!, heapDumpFile, durationMillis, reason))
InternalLeakCanary.sendEvent(HeapDump(currentEventUniqueId, heapDumpFile, durationMillis, reason))
} catch (throwable: Throwable) {
InternalLeakCanary.sendEvent(HeapDumpFailed(currentEventUniqueId!!, throwable, retry))
InternalLeakCanary.sendEvent(HeapDumpFailed(currentEventUniqueId, throwable, retry))
if (retry) {
scheduleRetainedObjectCheck(
delayMillis = WAIT_AFTER_DUMP_FAILED_MILLIS
Expand Down Expand Up @@ -237,32 +219,13 @@ internal class HeapDumpTrigger(

fun onDumpHeapReceived(forceDump: Boolean) {
backgroundHandler.post {
dismissNoRetainedOnTapNotification()
InternalLeakCanary.sendEvent(DismissNoRetainedOnTapNotification(currentEventUniqueId))
gcTrigger.runGc()
val retainedReferenceCount = objectWatcher.retainedObjectCount
if (!forceDump && retainedReferenceCount == 0) {
SharkLog.d { "Ignoring user request to dump heap: no retained objects remaining after GC" }
@Suppress("DEPRECATION")
val builder = Notification.Builder(application)
.setContentTitle(
application.getString(R.string.leak_canary_notification_no_retained_object_title)
)
.setContentText(
application.getString(
R.string.leak_canary_notification_no_retained_object_content
)
)
.setAutoCancel(true)
.setContentIntent(NotificationReceiver.pendingIntent(application, CANCEL_NOTIFICATION))
val notification =
Notifications.buildNotification(application, builder, LEAKCANARY_LOW)
notificationManager.notify(
R.id.leak_canary_notification_no_retained_object_on_tap, notification
)
backgroundHandler.postDelayed(
scheduleDismissNoRetainedOnTapNotification,
DISMISS_NO_RETAINED_OBJECT_NOTIFICATION_MILLIS
)
InternalLeakCanary.sendEvent(ShowNoMoreRetainedObjectFoundNotification(currentEventUniqueId))

lastDisplayedRetainedObjectCount = 0
return@post
}
Expand Down Expand Up @@ -359,64 +322,25 @@ internal class HeapDumpTrigger(
}

private fun showNoMoreRetainedObjectNotification() {
backgroundHandler.removeCallbacks(scheduleDismissRetainedCountNotification)
if (!Notifications.canShowNotification) {
return
}
val builder = Notification.Builder(application)
.setContentTitle(
application.getString(R.string.leak_canary_notification_no_retained_object_title)
)
.setContentText(
application.getString(
R.string.leak_canary_notification_no_retained_object_content
)
)
.setAutoCancel(true)
.setContentIntent(NotificationReceiver.pendingIntent(application, CANCEL_NOTIFICATION))
val notification =
Notifications.buildNotification(application, builder, LEAKCANARY_LOW)
notificationManager.notify(R.id.leak_canary_notification_retained_objects, notification)
backgroundHandler.postDelayed(
scheduleDismissRetainedCountNotification, DISMISS_NO_RETAINED_OBJECT_NOTIFICATION_MILLIS
)
InternalLeakCanary.sendEvent(ShowNoMoreRetainedObjectFoundNotification(currentEventUniqueId))
}

private fun showRetainedCountNotification(
objectCount: Int,
contentText: String
) {
backgroundHandler.removeCallbacks(scheduleDismissRetainedCountNotification)
if (!Notifications.canShowNotification) {
return
}
@Suppress("DEPRECATION")
val builder = Notification.Builder(application)
.setContentTitle(
application.getString(R.string.leak_canary_notification_retained_title, objectCount)
InternalLeakCanary.sendEvent(
EventListener.Event.ShowRetainedCountNotification(
uniqueId = currentEventUniqueId,
objectCount = objectCount,
contentText = contentText
)
.setContentText(contentText)
.setAutoCancel(true)
.setContentIntent(NotificationReceiver.pendingIntent(application, DUMP_HEAP))
val notification =
Notifications.buildNotification(application, builder, LEAKCANARY_LOW)
notificationManager.notify(R.id.leak_canary_notification_retained_objects, notification)
}

private fun dismissRetainedCountNotification() {
backgroundHandler.removeCallbacks(scheduleDismissRetainedCountNotification)
notificationManager.cancel(R.id.leak_canary_notification_retained_objects)
}

private fun dismissNoRetainedOnTapNotification() {
backgroundHandler.removeCallbacks(scheduleDismissNoRetainedOnTapNotification)
notificationManager.cancel(R.id.leak_canary_notification_no_retained_object_on_tap)
)
}

companion object {
internal const val WAIT_AFTER_DUMP_FAILED_MILLIS = 5_000L
private const val WAIT_FOR_OBJECT_THRESHOLD_MILLIS = 2_000L
private const val DISMISS_NO_RETAINED_OBJECT_NOTIFICATION_MILLIS = 30_000L
private const val WAIT_BETWEEN_HEAP_DUMPS_MILLIS = 60_000L
}
}