From ae2391037fac3e836df41748af378b8e4ced2ede Mon Sep 17 00:00:00 2001 From: Pierre-Yves Ricau Date: Thu, 10 Nov 2022 11:42:11 -0800 Subject: [PATCH 1/2] Add support for disabling LeakCanary notifications Fixes #2394 Fixes #2398 --- .../api/leakcanary-android-core.api | 11 +++++++---- .../src/main/java/leakcanary/LeakCanary.kt | 18 +++++++++++++++++- .../java/leakcanary/internal/Notifications.kt | 4 +++- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/leakcanary-android-core/api/leakcanary-android-core.api b/leakcanary-android-core/api/leakcanary-android-core.api index 57c3797cdf..f205c2421e 100644 --- a/leakcanary-android-core/api/leakcanary-android-core.api +++ b/leakcanary-android-core/api/leakcanary-android-core.api @@ -94,14 +94,15 @@ public final class leakcanary/LeakCanary { public final class leakcanary/LeakCanary$Config { public fun ()V - public fun (ZZILjava/util/List;Ljava/util/List;Lleakcanary/OnHeapAnalyzedListener;Lshark/MetadataExtractor;ZIZLshark/LeakingObjectFinder;Lleakcanary/HeapDumper;Ljava/util/List;Z)V - public synthetic fun (ZZILjava/util/List;Ljava/util/List;Lleakcanary/OnHeapAnalyzedListener;Lshark/MetadataExtractor;ZIZLshark/LeakingObjectFinder;Lleakcanary/HeapDumper;Ljava/util/List;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (ZZILjava/util/List;Ljava/util/List;Lleakcanary/OnHeapAnalyzedListener;Lshark/MetadataExtractor;ZIZLshark/LeakingObjectFinder;Lleakcanary/HeapDumper;Ljava/util/List;ZZ)V + public synthetic fun (ZZILjava/util/List;Ljava/util/List;Lleakcanary/OnHeapAnalyzedListener;Lshark/MetadataExtractor;ZIZLshark/LeakingObjectFinder;Lleakcanary/HeapDumper;Ljava/util/List;ZZILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Z public final fun component10 ()Z public final fun component11 ()Lshark/LeakingObjectFinder; public final fun component12 ()Lleakcanary/HeapDumper; public final fun component13 ()Ljava/util/List; public final fun component14 ()Z + public final fun component15 ()Z public final fun component2 ()Z public final fun component3 ()I public final fun component4 ()Ljava/util/List; @@ -110,8 +111,8 @@ public final class leakcanary/LeakCanary$Config { public final fun component7 ()Lshark/MetadataExtractor; public final fun component8 ()Z public final fun component9 ()I - public final fun copy (ZZILjava/util/List;Ljava/util/List;Lleakcanary/OnHeapAnalyzedListener;Lshark/MetadataExtractor;ZIZLshark/LeakingObjectFinder;Lleakcanary/HeapDumper;Ljava/util/List;Z)Lleakcanary/LeakCanary$Config; - public static synthetic fun copy$default (Lleakcanary/LeakCanary$Config;ZZILjava/util/List;Ljava/util/List;Lleakcanary/OnHeapAnalyzedListener;Lshark/MetadataExtractor;ZIZLshark/LeakingObjectFinder;Lleakcanary/HeapDumper;Ljava/util/List;ZILjava/lang/Object;)Lleakcanary/LeakCanary$Config; + public final fun copy (ZZILjava/util/List;Ljava/util/List;Lleakcanary/OnHeapAnalyzedListener;Lshark/MetadataExtractor;ZIZLshark/LeakingObjectFinder;Lleakcanary/HeapDumper;Ljava/util/List;ZZ)Lleakcanary/LeakCanary$Config; + public static synthetic fun copy$default (Lleakcanary/LeakCanary$Config;ZZILjava/util/List;Ljava/util/List;Lleakcanary/OnHeapAnalyzedListener;Lshark/MetadataExtractor;ZIZLshark/LeakingObjectFinder;Lleakcanary/HeapDumper;Ljava/util/List;ZZILjava/lang/Object;)Lleakcanary/LeakCanary$Config; public fun equals (Ljava/lang/Object;)Z public final fun getComputeRetainedHeapSize ()Z public final fun getDumpHeap ()Z @@ -126,6 +127,7 @@ public final class leakcanary/LeakCanary$Config { public final fun getReferenceMatchers ()Ljava/util/List; public final fun getRequestWriteExternalStoragePermission ()Z public final fun getRetainedVisibleThreshold ()I + public final fun getShowNotifications ()Z public final fun getUseExperimentalLeakFinders ()Z public fun hashCode ()I public final fun newBuilder ()Lleakcanary/LeakCanary$Config$Builder; @@ -147,6 +149,7 @@ public final class leakcanary/LeakCanary$Config$Builder { public final fun referenceMatchers (Ljava/util/List;)Lleakcanary/LeakCanary$Config$Builder; public final fun requestWriteExternalStoragePermission (Z)Lleakcanary/LeakCanary$Config$Builder; public final fun retainedVisibleThreshold (I)Lleakcanary/LeakCanary$Config$Builder; + public final fun showNotifications (Z)Lleakcanary/LeakCanary$Config$Builder; public final fun useExperimentalLeakFinders (Z)Lleakcanary/LeakCanary$Config$Builder; } diff --git a/leakcanary-android-core/src/main/java/leakcanary/LeakCanary.kt b/leakcanary-android-core/src/main/java/leakcanary/LeakCanary.kt index b7965f6089..96a27ef6bc 100644 --- a/leakcanary-android-core/src/main/java/leakcanary/LeakCanary.kt +++ b/leakcanary-android-core/src/main/java/leakcanary/LeakCanary.kt @@ -208,6 +208,15 @@ object LeakCanary { } ), + /** + * Whether to show LeakCanary notifications. When [showNotifications] is true, LeakCanary + * will only display notifications if the app is in foreground and is not an instant, TV or + * Wear app. + * + * Defaults to true. + */ + val showNotifications: Boolean = true, + /** * Deprecated: This is a no-op, set a custom [leakingObjectFinder] instead. */ @@ -256,6 +265,7 @@ object LeakCanary { private var heapDumper = config.heapDumper private var eventListeners = config.eventListeners private var useExperimentalLeakFinders = config.useExperimentalLeakFinders + private var showNotifications = config.showNotifications /** @see [LeakCanary.Config.dumpHeap] */ fun dumpHeap(dumpHeap: Boolean) = @@ -315,6 +325,11 @@ object LeakCanary { fun useExperimentalLeakFinders(useExperimentalLeakFinders: Boolean) = apply { this.useExperimentalLeakFinders = useExperimentalLeakFinders } + /** @see [LeakCanary.Config.showNotifications] */ + fun showNotifications(showNotifications: Boolean) = + apply { this.showNotifications = showNotifications } + + fun build() = config.copy( dumpHeap = dumpHeap, dumpHeapWhenDebugging = dumpHeapWhenDebugging, @@ -329,7 +344,8 @@ object LeakCanary { leakingObjectFinder = leakingObjectFinder, heapDumper = heapDumper, eventListeners = eventListeners, - useExperimentalLeakFinders = useExperimentalLeakFinders + useExperimentalLeakFinders = useExperimentalLeakFinders, + showNotifications = showNotifications, ) } } diff --git a/leakcanary-android-core/src/main/java/leakcanary/internal/Notifications.kt b/leakcanary-android-core/src/main/java/leakcanary/internal/Notifications.kt index 41edceebc9..ef4e0f9334 100644 --- a/leakcanary-android-core/src/main/java/leakcanary/internal/Notifications.kt +++ b/leakcanary-android-core/src/main/java/leakcanary/internal/Notifications.kt @@ -24,6 +24,7 @@ import android.os.Build.VERSION.SDK_INT import android.os.Build.VERSION_CODES.JELLY_BEAN import android.os.Build.VERSION_CODES.O import com.squareup.leakcanary.core.R +import leakcanary.LeakCanary import leakcanary.internal.InternalLeakCanary.FormFactor.MOBILE internal object Notifications { @@ -34,7 +35,8 @@ internal object Notifications { // Watch devices: not sure, but probably not a good idea anyway? val canShowNotification: Boolean get() = InternalLeakCanary.formFactor == MOBILE && - (!InternalLeakCanary.isInstantApp || InternalLeakCanary.applicationVisible) + (!InternalLeakCanary.isInstantApp || InternalLeakCanary.applicationVisible) && + LeakCanary.config.showNotifications @Suppress("LongParameterList") fun showNotification( From 9e3d3ee03e597cdc42d154597067417b47a8931e Mon Sep 17 00:00:00 2001 From: Pierre-Yves Ricau Date: Thu, 10 Nov 2022 13:24:05 -0800 Subject: [PATCH 2/2] Request permission on Android 13 --- build.gradle | 2 +- .../src/main/AndroidManifest.xml | 7 +-- .../internal/LeakDirectoryProvider.kt | 5 ++- .../java/leakcanary/internal/Notifications.kt | 45 +++++++++++++++++-- ...tivity.kt => RequestPermissionActivity.kt} | 29 +++++++----- .../main/res/values/leak_canary_strings.xml | 2 +- 6 files changed, 70 insertions(+), 20 deletions(-) rename leakcanary-android-core/src/main/java/leakcanary/internal/{RequestStoragePermissionActivity.kt => RequestPermissionActivity.kt} (70%) diff --git a/build.gradle b/build.gradle index 4090f8c0a4..28731ee557 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ import org.jetbrains.dokka.gradle.DokkaTask buildscript { ext.versions = [ 'minSdk' : 14, - 'compileSdk': 31, + 'compileSdk': 33, ] repositories { google() diff --git a/leakcanary-android-core/src/main/AndroidManifest.xml b/leakcanary-android-core/src/main/AndroidManifest.xml index 311285aa35..5e0b6d0401 100644 --- a/leakcanary-android-core/src/main/AndroidManifest.xml +++ b/leakcanary-android-core/src/main/AndroidManifest.xml @@ -22,8 +22,9 @@ - - + + + = 33) { + val application = InternalLeakCanary.application + if (application.applicationInfo.targetSdkVersion >= 33) { + val notificationManager = + application.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + if (!notificationManager.areNotificationsEnabled()) { + if (notificationPermissionRequested) { + SharkLog.d { "Not showing notification: already requested missing POST_NOTIFICATIONS permission." } + } else { + SharkLog.d { "Not showing notification: requesting missing POST_NOTIFICATIONS permission." } + application.startActivity( + RequestPermissionActivity.createIntent( + application, + POST_NOTIFICATIONS + ) + ) + notificationPermissionRequested = true + } + return false + } + if (notificationManager.areNotificationsPaused()) { + SharkLog.d { "Not showing notification, notifications are paused." } + return false + } + } + } + return true + } @Suppress("LongParameterList") fun showNotification( diff --git a/leakcanary-android-core/src/main/java/leakcanary/internal/RequestStoragePermissionActivity.kt b/leakcanary-android-core/src/main/java/leakcanary/internal/RequestPermissionActivity.kt similarity index 70% rename from leakcanary-android-core/src/main/java/leakcanary/internal/RequestStoragePermissionActivity.kt rename to leakcanary-android-core/src/main/java/leakcanary/internal/RequestPermissionActivity.kt index 22329f1d87..74b47eefa1 100644 --- a/leakcanary-android-core/src/main/java/leakcanary/internal/RequestStoragePermissionActivity.kt +++ b/leakcanary-android-core/src/main/java/leakcanary/internal/RequestPermissionActivity.kt @@ -15,7 +15,6 @@ */ package leakcanary.internal -import android.Manifest.permission.WRITE_EXTERNAL_STORAGE import android.annotation.TargetApi import android.app.Activity import android.app.PendingIntent @@ -33,17 +32,20 @@ import android.widget.Toast.LENGTH_LONG import com.squareup.leakcanary.core.R @TargetApi(Build.VERSION_CODES.M) // -internal class RequestStoragePermissionActivity : Activity() { +internal class RequestPermissionActivity : Activity() { + + private val targetPermission: String + get() = intent.getStringExtra(TARGET_PERMISSION_EXTRA)!! override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (savedInstanceState == null) { - if (hasStoragePermission()) { + if (hasTargetPermission()) { finish() return } - val permissions = arrayOf(WRITE_EXTERNAL_STORAGE) + val permissions = arrayOf(targetPermission) requestPermissions(permissions, 42) } } @@ -53,7 +55,7 @@ internal class RequestStoragePermissionActivity : Activity() { permissions: Array, grantResults: IntArray ) { - if (!hasStoragePermission()) { + if (!hasTargetPermission()) { Toast.makeText(application, R.string.leak_canary_permission_not_granted, LENGTH_LONG) .show() } @@ -66,15 +68,22 @@ internal class RequestStoragePermissionActivity : Activity() { super.finish() } - private fun hasStoragePermission(): Boolean { - return checkSelfPermission(WRITE_EXTERNAL_STORAGE) == PERMISSION_GRANTED + private fun hasTargetPermission(): Boolean { + return checkSelfPermission(targetPermission) == PERMISSION_GRANTED } companion object { + private const val TARGET_PERMISSION_EXTRA = "targetPermission" + + fun createIntent(context: Context, permission: String): Intent { + return Intent(context, RequestPermissionActivity::class.java).apply { + flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TOP + putExtra(TARGET_PERMISSION_EXTRA, permission) + } + } - fun createPendingIntent(context: Context): PendingIntent { - val intent = Intent(context, RequestStoragePermissionActivity::class.java) - intent.flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TOP + fun createPendingIntent(context: Context, permission: String): PendingIntent { + val intent = createIntent(context, permission) val flags = if (Build.VERSION.SDK_INT >= 23) { FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE } else { diff --git a/leakcanary-android-core/src/main/res/values/leak_canary_strings.xml b/leakcanary-android-core/src/main/res/values/leak_canary_strings.xml index 06b6540511..39f75a4cc3 100644 --- a/leakcanary-android-core/src/main/res/values/leak_canary_strings.xml +++ b/leakcanary-android-core/src/main/res/values/leak_canary_strings.xml @@ -63,7 +63,7 @@ Delete Delete all Are you sure you want to delete all leaks? - Please grant external storage permission, otherwise memory leaks will not be detected. + Please grant requested permission, otherwise memory leak detection may not work. Leak detected, need permission Click to enable storage permission for %s. Leaks