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

Add support for disabling LeakCanary notifications #2440

Merged
merged 2 commits into from Nov 10, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion build.gradle
Expand Up @@ -3,7 +3,7 @@ import org.jetbrains.dokka.gradle.DokkaTask
buildscript {
ext.versions = [
'minSdk' : 14,
'compileSdk': 31,
'compileSdk': 33,
]
repositories {
google()
Expand Down
11 changes: 7 additions & 4 deletions leakcanary-android-core/api/leakcanary-android-core.api
Expand Up @@ -94,14 +94,15 @@ public final class leakcanary/LeakCanary {

public final class leakcanary/LeakCanary$Config {
public fun <init> ()V
public fun <init> (ZZILjava/util/List;Ljava/util/List;Lleakcanary/OnHeapAnalyzedListener;Lshark/MetadataExtractor;ZIZLshark/LeakingObjectFinder;Lleakcanary/HeapDumper;Ljava/util/List;Z)V
public synthetic fun <init> (ZZILjava/util/List;Ljava/util/List;Lleakcanary/OnHeapAnalyzedListener;Lshark/MetadataExtractor;ZIZLshark/LeakingObjectFinder;Lleakcanary/HeapDumper;Ljava/util/List;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (ZZILjava/util/List;Ljava/util/List;Lleakcanary/OnHeapAnalyzedListener;Lshark/MetadataExtractor;ZIZLshark/LeakingObjectFinder;Lleakcanary/HeapDumper;Ljava/util/List;ZZ)V
public synthetic fun <init> (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;
Expand All @@ -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
Expand All @@ -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;
Expand All @@ -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;
}

Expand Down
7 changes: 4 additions & 3 deletions leakcanary-android-core/src/main/AndroidManifest.xml
Expand Up @@ -22,8 +22,9 @@
<!-- To store the heap dumps and leak analysis results. -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- To allow starting foreground services on Android P+ - https://developer.android.com/preview/behavior-changes#fg-svc -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>

<!-- To allow posting notifications on Android 13 -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>

<application>
<provider
Expand Down Expand Up @@ -92,7 +93,7 @@
</activity-alias>

<activity
android:name="leakcanary.internal.RequestStoragePermissionActivity"
android:name="leakcanary.internal.RequestPermissionActivity"
android:excludeFromRecents="true"
android:icon="@mipmap/leak_canary_icon"
android:label="@string/leak_canary_storage_permission_activity_label"
Expand Down
18 changes: 17 additions & 1 deletion leakcanary-android-core/src/main/java/leakcanary/LeakCanary.kt
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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) =
Expand Down Expand Up @@ -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,
Expand All @@ -329,7 +344,8 @@ object LeakCanary {
leakingObjectFinder = leakingObjectFinder,
heapDumper = heapDumper,
eventListeners = eventListeners,
useExperimentalLeakFinders = useExperimentalLeakFinders
useExperimentalLeakFinders = useExperimentalLeakFinders,
showNotifications = showNotifications,
)
}
}
Expand Down
Expand Up @@ -111,12 +111,13 @@ internal class LeakDirectoryProvider constructor(
}

fun requestWritePermissionNotification() {
if (permissionNotificationDisplayed) {
if (permissionNotificationDisplayed || !Notifications.canShowNotification) {
return
}
permissionNotificationDisplayed = true

val pendingIntent = RequestStoragePermissionActivity.createPendingIntent(context)
val pendingIntent =
RequestPermissionActivity.createPendingIntent(context, WRITE_EXTERNAL_STORAGE)
val contentTitle = context.getString(
R.string.leak_canary_permission_notification_title
)
Expand Down
Expand Up @@ -15,6 +15,7 @@
*/
package leakcanary.internal

import android.Manifest.permission.POST_NOTIFICATIONS
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
Expand All @@ -24,17 +25,57 @@ 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
import shark.SharkLog

internal object Notifications {

private var notificationPermissionRequested = false

// Instant apps cannot show background notifications
// See https://github.com/square/leakcanary/issues/1197
// TV devices can't show notifications.
// Watch devices: not sure, but probably not a good idea anyway?
val canShowNotification: Boolean
get() = InternalLeakCanary.formFactor == MOBILE &&
(!InternalLeakCanary.isInstantApp || InternalLeakCanary.applicationVisible)
get() {
if (InternalLeakCanary.formFactor != MOBILE) {
return false
}
if (InternalLeakCanary.isInstantApp || !InternalLeakCanary.applicationVisible) {
return false
}
if (!LeakCanary.config.showNotifications) {
return false
}
if (SDK_INT >= 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(
Expand Down
Expand Up @@ -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
Expand All @@ -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)
}
}
Expand All @@ -53,7 +55,7 @@ internal class RequestStoragePermissionActivity : Activity() {
permissions: Array<String>,
grantResults: IntArray
) {
if (!hasStoragePermission()) {
if (!hasTargetPermission()) {
Toast.makeText(application, R.string.leak_canary_permission_not_granted, LENGTH_LONG)
.show()
}
Expand All @@ -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 {
Expand Down
Expand Up @@ -63,7 +63,7 @@
<string name="leak_canary_delete">Delete</string>
<string name="leak_canary_delete_all">Delete all</string>
<string name="leak_canary_delete_all_leaks_title">Are you sure you want to delete all leaks?</string>
<string name="leak_canary_permission_not_granted">Please grant external storage permission, otherwise memory leaks will not be detected.</string>
<string name="leak_canary_permission_not_granted">Please grant requested permission, otherwise memory leak detection may not work.</string>
<string name="leak_canary_permission_notification_title">Leak detected, need permission</string>
<string name="leak_canary_permission_notification_text">Click to enable storage permission for %s.</string>
<string name="leak_canary_shortcut_label">Leaks</string>
Expand Down