mirror of
https://github.com/square/leakcanary.git
synced 2026-03-13 08:24:22 +08:00
Merge pull request #2806 from blundell/update-min-sdk-to-api-21
Update minimum SDK to API 26 (Android 8.0 Oreo)
This commit is contained in:
11
.github/workflows/main.yml
vendored
11
.github/workflows/main.yml
vendored
@@ -36,21 +36,10 @@ jobs:
|
||||
target: [ google_apis ]
|
||||
channel: [ stable ]
|
||||
api-level:
|
||||
- 21
|
||||
- 23
|
||||
- 26
|
||||
- 31
|
||||
- 33
|
||||
- 34
|
||||
include:
|
||||
- arch: x86
|
||||
api-level: 16
|
||||
target: google_apis
|
||||
channel: stable
|
||||
- arch: x86
|
||||
api-level: 19
|
||||
target: google_apis
|
||||
channel: stable
|
||||
steps:
|
||||
- name: Enable KVM group perms
|
||||
run: |
|
||||
|
||||
@@ -23,7 +23,7 @@ androidXJunit = "1.1.5"
|
||||
workManager = "2.7.0"
|
||||
detekt = "1.23.8"
|
||||
hilt = "2.53"
|
||||
androidMinSdk = "14"
|
||||
androidMinSdk = "26"
|
||||
androidCompileSdk = "35"
|
||||
|
||||
[libraries]
|
||||
@@ -60,7 +60,7 @@ androidX-test-monitor = { module = "androidx.test:monitor", version = "1.6.1" }
|
||||
androidX-test-rules = { module = "androidx.test:rules", version.ref = "androidXTest" }
|
||||
# Exposed transitively, avoid increasing
|
||||
androidX-test-runner = { module = "androidx.test:runner", version = "1.5.2" }
|
||||
androidX-test-orchestrator = { module = "androidx.test:orchestrator", version = "1.4.2" } # 1.5.0+ requires API 21+, keeping at 1.4.2 for API 16 compatibility
|
||||
androidX-test-orchestrator = { module = "androidx.test:orchestrator", version = "1.5.0" }
|
||||
androidX-test-espresso = { module = "androidx.test.espresso:espresso-core", version = "3.5.1" }
|
||||
androidX-test-junit = { module = "androidx.test.ext:junit", version.ref = "androidXJunit" }
|
||||
androidX-test-junitKtx = { module = "androidx.test.ext:junit-ktx", version.ref = "androidXJunit" }
|
||||
|
||||
@@ -7,8 +7,6 @@ import android.content.Intent
|
||||
import android.content.pm.ShortcutInfo.Builder
|
||||
import android.content.pm.ShortcutManager
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build.VERSION
|
||||
import android.os.Build.VERSION_CODES
|
||||
import com.squareup.leakcanary.core.R
|
||||
import shark.SharkLog
|
||||
|
||||
@@ -18,9 +16,6 @@ internal object LeakCanaryAndroidInternalUtils {
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
fun addLeakActivityDynamicShortcut(application: Application) {
|
||||
if (VERSION.SDK_INT < VERSION_CODES.N_MR1) {
|
||||
return
|
||||
}
|
||||
if (!application.resources.getBoolean(R.bool.leak_canary_add_dynamic_shortcut)) {
|
||||
return
|
||||
}
|
||||
@@ -124,6 +119,6 @@ internal object LeakCanaryAndroidInternalUtils {
|
||||
}
|
||||
|
||||
fun isInstantApp(application: Application): Boolean {
|
||||
return VERSION.SDK_INT >= VERSION_CODES.O && application.packageManager.isInstantApp
|
||||
return application.packageManager.isInstantApp
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.app.Notification
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import com.squareup.leakcanary.core.R
|
||||
import leakcanary.EventListener.Event
|
||||
import leakcanary.EventListener.Event.DumpingHeap
|
||||
@@ -67,11 +66,7 @@ object NotificationEventListener : EventListener {
|
||||
} else {
|
||||
appContext.getString(R.string.leak_canary_analysis_failed)
|
||||
}
|
||||
val flags = if (Build.VERSION.SDK_INT >= 23) {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
} else {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
}
|
||||
val flags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
val pendingIntent = PendingIntent.getActivity(appContext, 1, event.showIntent, flags)
|
||||
showHeapAnalysisResultNotification(contentTitle,pendingIntent)
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import android.content.pm.ProviderInfo
|
||||
import android.database.Cursor
|
||||
import android.database.MatrixCursor
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.os.StrictMode
|
||||
@@ -499,7 +498,7 @@ internal class LeakCanaryFileProvider : ContentProvider() {
|
||||
if (externalCacheDirs.isNotEmpty()) {
|
||||
target = externalCacheDirs[0]
|
||||
}
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && TAG_EXTERNAL_MEDIA == tag) {
|
||||
} else if (TAG_EXTERNAL_MEDIA == tag) {
|
||||
val externalMediaDirs = context.externalMediaDirs
|
||||
if (externalMediaDirs.isNotEmpty()) {
|
||||
target = externalMediaDirs[0]
|
||||
@@ -519,19 +518,11 @@ internal class LeakCanaryFileProvider : ContentProvider() {
|
||||
context: Context,
|
||||
type: String?
|
||||
): Array<File> {
|
||||
return if (Build.VERSION.SDK_INT >= 19) {
|
||||
context.getExternalFilesDirs(type)
|
||||
} else {
|
||||
arrayOf(context.getExternalFilesDir(type)!!)
|
||||
}
|
||||
return context.getExternalFilesDirs(type)
|
||||
}
|
||||
|
||||
private fun getExternalCacheDirs(context: Context): Array<File> {
|
||||
return if (Build.VERSION.SDK_INT >= 19) {
|
||||
context.externalCacheDirs
|
||||
} else {
|
||||
arrayOf(context.externalCacheDir!!)
|
||||
}
|
||||
return context.externalCacheDirs
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,11 +16,8 @@
|
||||
package leakcanary.internal
|
||||
|
||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
import android.annotation.TargetApi
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.os.Build.VERSION_CODES.M
|
||||
import android.os.Environment
|
||||
import android.os.Environment.DIRECTORY_DOWNLOADS
|
||||
import com.squareup.leakcanary.core.R
|
||||
@@ -79,12 +76,7 @@ internal class LeakDirectoryProvider constructor(
|
||||
return File(storageDirectory, fileName)
|
||||
}
|
||||
|
||||
@TargetApi(M) fun hasStoragePermission(): Boolean {
|
||||
// Defensive check: @TargetApi doesn't prevent this method from being called on older APIs
|
||||
@Suppress("ObsoleteSdkInt")
|
||||
if (SDK_INT < M) {
|
||||
return true
|
||||
}
|
||||
fun hasStoragePermission(): Boolean {
|
||||
// Once true, this won't change for the life of the process so we can cache it.
|
||||
if (writeExternalStorageGranted) {
|
||||
return true
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import leakcanary.internal.NotificationReceiver.Action.CANCEL_NOTIFICATION
|
||||
import leakcanary.internal.NotificationReceiver.Action.DUMP_HEAP
|
||||
import shark.SharkLog
|
||||
@@ -40,11 +39,7 @@ internal class NotificationReceiver : BroadcastReceiver() {
|
||||
): PendingIntent {
|
||||
val broadcastIntent = Intent(context, NotificationReceiver::class.java)
|
||||
broadcastIntent.action = action.name
|
||||
val flags = if (Build.VERSION.SDK_INT >= 23) {
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
} else {
|
||||
0
|
||||
}
|
||||
val flags = PendingIntent.FLAG_IMMUTABLE
|
||||
return PendingIntent.getBroadcast(context, 0, broadcastIntent, flags)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,6 @@ import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
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
|
||||
@@ -90,9 +88,7 @@ internal object Notifications {
|
||||
return
|
||||
}
|
||||
|
||||
val builder = if (SDK_INT >= O) {
|
||||
Notification.Builder(context, type.name)
|
||||
} else Notification.Builder(context)
|
||||
val builder = Notification.Builder(context, type.name)
|
||||
|
||||
builder
|
||||
.setContentText(contentText)
|
||||
@@ -115,26 +111,19 @@ internal object Notifications {
|
||||
builder.setSmallIcon(R.drawable.leak_canary_leak)
|
||||
.setWhen(System.currentTimeMillis())
|
||||
|
||||
if (SDK_INT >= O) {
|
||||
val notificationManager =
|
||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
var notificationChannel: NotificationChannel? =
|
||||
notificationManager.getNotificationChannel(type.name)
|
||||
if (notificationChannel == null) {
|
||||
val channelName = context.getString(type.nameResId)
|
||||
notificationChannel =
|
||||
NotificationChannel(type.name, channelName, type.importance)
|
||||
notificationManager.createNotificationChannel(notificationChannel)
|
||||
}
|
||||
builder.setChannelId(type.name)
|
||||
builder.setGroup(type.name)
|
||||
val notificationManager =
|
||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
var notificationChannel: NotificationChannel? =
|
||||
notificationManager.getNotificationChannel(type.name)
|
||||
if (notificationChannel == null) {
|
||||
val channelName = context.getString(type.nameResId)
|
||||
notificationChannel =
|
||||
NotificationChannel(type.name, channelName, type.importance)
|
||||
notificationManager.createNotificationChannel(notificationChannel)
|
||||
}
|
||||
builder.setChannelId(type.name)
|
||||
builder.setGroup(type.name)
|
||||
|
||||
return if (SDK_INT < JELLY_BEAN) {
|
||||
@Suppress("DEPRECATION")
|
||||
builder.notification
|
||||
} else {
|
||||
builder.build()
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
package leakcanary.internal
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.app.Activity
|
||||
import android.app.PendingIntent
|
||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
@@ -26,12 +25,10 @@ import android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||
import android.os.Bundle
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
import android.widget.Toast.LENGTH_LONG
|
||||
import com.squareup.leakcanary.core.R
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.M) //
|
||||
internal class RequestPermissionActivity : Activity() {
|
||||
|
||||
private val targetPermission: String
|
||||
@@ -84,13 +81,7 @@ internal class RequestPermissionActivity : Activity() {
|
||||
|
||||
fun createPendingIntent(context: Context, permission: String): PendingIntent {
|
||||
val intent = createIntent(context, permission)
|
||||
// Defensive check: @TargetApi on class doesn't prevent this method from being called on older APIs
|
||||
@Suppress("ObsoleteSdkInt")
|
||||
val flags = if (Build.VERSION.SDK_INT >= 23) {
|
||||
FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE
|
||||
} else {
|
||||
FLAG_UPDATE_CURRENT
|
||||
}
|
||||
val flags = FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE
|
||||
return PendingIntent.getActivity(context, 1, intent, flags)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Path
|
||||
import android.os.Build
|
||||
import android.text.Layout
|
||||
import com.squareup.leakcanary.core.R
|
||||
import kotlin.math.max
|
||||
@@ -107,7 +106,7 @@ internal abstract class SquigglySpanRenderer(context: Context) {
|
||||
|
||||
private fun Layout.getLineBottomWithoutSpacing(line: Int): Int {
|
||||
val lineBottom = getLineBottom(line)
|
||||
val lastLineSpacingNotAdded = Build.VERSION.SDK_INT >= 19
|
||||
val lastLineSpacingNotAdded = true
|
||||
val isLastLine = line == lineCount - 1
|
||||
|
||||
val lineBottomWithoutSpacing: Int
|
||||
|
||||
@@ -6,7 +6,7 @@ import kotlin.concurrent.getOrSet
|
||||
|
||||
/**
|
||||
* Similar to the more generic use() for Closable.
|
||||
* Cursor started implementing Closable in API 16.
|
||||
* Cursor implements Closable on all supported API levels (26+).
|
||||
*/
|
||||
internal inline fun <R> Cursor.use(block: (Cursor) -> R): R {
|
||||
var exception: Throwable? = null
|
||||
|
||||
@@ -2,8 +2,6 @@ package leakcanary.internal.activity.screen
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build.VERSION
|
||||
import android.os.Build.VERSION_CODES
|
||||
import android.os.Environment
|
||||
import android.os.Environment.DIRECTORY_DOWNLOADS
|
||||
import android.view.View
|
||||
@@ -60,11 +58,7 @@ internal class RenderHeapDumpScreen(
|
||||
imageView.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
|
||||
viewTreeObserver.removeOnGlobalLayoutListener(this)
|
||||
} else {
|
||||
viewTreeObserver.removeGlobalOnLayoutListener(this)
|
||||
}
|
||||
viewTreeObserver.removeOnGlobalLayoutListener(this)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package leakcanary.internal.navigation
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.Menu
|
||||
@@ -155,12 +154,10 @@ internal abstract class NavigatingActivity : Activity() {
|
||||
|
||||
private fun screenUpdated() {
|
||||
invalidateOptionsMenu()
|
||||
if (SDK_INT >= 18) {
|
||||
actionBar?.run {
|
||||
val goBack = backstack.size > 0
|
||||
val indicator = if (goBack) 0 else android.R.drawable.ic_menu_close_clear_cancel
|
||||
setHomeAsUpIndicator(indicator)
|
||||
}
|
||||
actionBar?.run {
|
||||
val goBack = backstack.size > 0
|
||||
val indicator = if (goBack) 0 else android.R.drawable.ic_menu_close_clear_cancel
|
||||
setHomeAsUpIndicator(indicator)
|
||||
}
|
||||
onNewScreen(currentScreen)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package leakcanary.internal.navigation
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.os.Build.VERSION
|
||||
import android.os.Parcelable
|
||||
import android.util.SparseArray
|
||||
import android.view.LayoutInflater
|
||||
@@ -41,11 +40,7 @@ internal fun View.goBack() {
|
||||
}
|
||||
|
||||
internal fun Context.getColorCompat(id: Int): Int {
|
||||
return if (VERSION.SDK_INT >= 23) {
|
||||
getColor(id)
|
||||
} else {
|
||||
resources.getColor(id)
|
||||
}
|
||||
return getColor(id)
|
||||
}
|
||||
|
||||
internal fun View.onScreenExiting(block: () -> Unit) {
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="?android:attr/colorControlHighlight">
|
||||
<item android:id="@android:id/mask">
|
||||
<shape>
|
||||
<solid android:color="@color/leak_canary_gray" />
|
||||
<corners android:radius="20dp" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<shape>
|
||||
<solid android:color="@color/leak_canary_gray" />
|
||||
<corners android:radius="20dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
||||
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_focused="true" android:state_pressed="false">
|
||||
<color android:color="?android:attr/colorControlHighlight" />
|
||||
</item>
|
||||
<item>
|
||||
<color android:color="@android:color/transparent" />
|
||||
</item>
|
||||
</selector>
|
||||
@@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="@color/leak_canary_yellow_button_pressed">
|
||||
<item>
|
||||
<shape>
|
||||
<solid android:color="@color/leak_canary_yellow_button" />
|
||||
<corners android:radius="12dp" />
|
||||
</shape>
|
||||
</item>
|
||||
<item android:id="@android:id/mask">
|
||||
<shape>
|
||||
<solid android:color="@color/leak_canary_yellow_button" />
|
||||
<corners android:radius="12dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="@color/leak_canary_gray_6f">
|
||||
<item>
|
||||
<shape>
|
||||
<solid android:color="@color/leak_canary_gray_3f" />
|
||||
<corners android:radius="12dp" />
|
||||
</shape>
|
||||
</item>
|
||||
<item android:id="@android:id/mask">
|
||||
<shape>
|
||||
<solid android:color="@color/leak_canary_gray_3f" />
|
||||
<corners android:radius="12dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/leak_canary_tab_selector_ripple" />
|
||||
</selector>
|
||||
@@ -1,8 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_pressed="true">
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="?android:attr/colorControlHighlight">
|
||||
<item android:id="@android:id/mask">
|
||||
<shape>
|
||||
<solid android:color="@color/leak_canary_gray_3f" />
|
||||
<solid android:color="@color/leak_canary_gray" />
|
||||
<corners android:radius="20dp" />
|
||||
</shape>
|
||||
</item>
|
||||
@@ -12,4 +13,4 @@
|
||||
<corners android:radius="20dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
</ripple>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_focused="true" android:state_pressed="false">
|
||||
<color android:color="@color/leak_canary_gray_3f" />
|
||||
<color android:color="?android:attr/colorControlHighlight" />
|
||||
</item>
|
||||
<item>
|
||||
<color android:color="@android:color/transparent" />
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_pressed="true">
|
||||
<shape>
|
||||
<solid android:color="@color/leak_canary_yellow_button_pressed" />
|
||||
<corners android:radius="12dp" />
|
||||
</shape>
|
||||
</item>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="@color/leak_canary_yellow_button_pressed">
|
||||
<item>
|
||||
<shape>
|
||||
<solid android:color="@color/leak_canary_yellow_button" />
|
||||
<corners android:radius="12dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
</ripple>
|
||||
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_pressed="true">
|
||||
<shape>
|
||||
<solid android:color="@color/leak_canary_gray_6f" />
|
||||
<corners android:radius="12dp" />
|
||||
</shape>
|
||||
</item>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="@color/leak_canary_gray_6f">
|
||||
<item>
|
||||
<shape>
|
||||
<solid android:color="@color/leak_canary_gray_3f" />
|
||||
<corners android:radius="12dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
</ripple>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_pressed="false" android:drawable="@android:color/transparent" />
|
||||
<item android:drawable="@color/leak_canary_gray_darkest_25p" />
|
||||
</selector>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="@color/leak_canary_gray_darkest_25p">
|
||||
<item android:drawable="@android:color/transparent" />
|
||||
</ripple>
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
<ripple
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="@color/leak_canary_gray_darkest_25p"
|
||||
/>
|
||||
/>
|
||||
@@ -1,28 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (C) 2015 Square, Inc.
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
<style name="leak_canary_LeakCanary.Base" parent="android:Theme.Material">
|
||||
<item name="android:windowBackground">@color/leak_canary_background_color</item>
|
||||
<item name="android:actionBarStyle">@style/leak_canary_Widget.ActionBar</item>
|
||||
</style>
|
||||
|
||||
<style name="leak_canary_Widget.ActionBar" parent="android:Widget.Material.ActionBar">
|
||||
<item name="android:background">@color/leak_canary_background_color</item>
|
||||
<item name="android:elevation">0dp</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
@@ -15,14 +15,14 @@
|
||||
~ limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
<style name="leak_canary_LeakCanary.Base" parent="android:Theme.Holo">
|
||||
<style name="leak_canary_LeakCanary.Base" parent="android:Theme.Material">
|
||||
<item name="android:windowBackground">@color/leak_canary_background_color</item>
|
||||
<item name="android:actionBarStyle">@style/leak_canary_Widget.ActionBar</item>
|
||||
</style>
|
||||
|
||||
<style name="leak_canary_Widget.ActionBar" parent="android:Widget.Holo.ActionBar">
|
||||
<style name="leak_canary_Widget.ActionBar" parent="android:Widget.Material.ActionBar">
|
||||
<item name="android:background">@color/leak_canary_background_color</item>
|
||||
<item name="android:windowContentOverlay">@null</item>
|
||||
<item name="android:elevation">0dp</item>
|
||||
</style>
|
||||
|
||||
<style name="leak_canary_Theme.Transparent" parent="android:Theme">
|
||||
|
||||
@@ -64,19 +64,16 @@ class TestDescriptionHolderTest {
|
||||
|
||||
@Test
|
||||
fun testDescriptionThrowsBeforeClass() {
|
||||
// On API 16, assertThat(Throwable) causes a TypeComparators class init failure.
|
||||
assertThat(beforeClassThrowable == null).isFalse()
|
||||
assertThat(beforeClassThrowable).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDescriptionThrowsBeforeOuterEvaluate() {
|
||||
// On API 16, assertThat(Throwable) causes a TypeComparators class init failure.
|
||||
assertThat(outerRule.beforeEvaluateThrowable == null).isFalse()
|
||||
assertThat(outerRule.beforeEvaluateThrowable).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDescriptionDoesNotThrowBeforeInnerEvaluate() {
|
||||
// On API 16, assertThat(Throwable) causes a TypeComparators class init failure.
|
||||
assertThat(innerRule.beforeEvaluateThrowable == null).isTrue()
|
||||
assertThat(innerRule.beforeEvaluateThrowable).isNull()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ android {
|
||||
resourcePrefix = "leak_canary_"
|
||||
compileSdk = libs.versions.androidCompileSdk.get().toInt()
|
||||
defaultConfig {
|
||||
minSdk = 16
|
||||
minSdk = libs.versions.androidMinSdk.get().toInt()
|
||||
buildConfigField("String", "LIBRARY_VERSION", "\"${rootProject.property("VERSION_NAME")}\"")
|
||||
buildConfigField("String", "GIT_SHA", "\"${gitSha()}\"")
|
||||
consumerProguardFiles("consumer-proguard-rules.pro")
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.app.ActivityManager
|
||||
import android.app.ActivityManager.MemoryInfo
|
||||
import android.app.ActivityManager.RunningAppProcessInfo
|
||||
import android.content.Context
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.os.Process
|
||||
import android.os.SystemClock
|
||||
import android.system.Os
|
||||
@@ -52,11 +51,7 @@ interface ProcessInfo {
|
||||
}
|
||||
|
||||
override val elapsedMillisSinceStart: Long
|
||||
get() = if (SDK_INT >= 24) {
|
||||
SystemClock.uptimeMillis() - processStartUptimeMillis
|
||||
} else {
|
||||
SystemClock.elapsedRealtime() - processForkRealtimeMillis
|
||||
}
|
||||
get() = SystemClock.uptimeMillis() - processStartUptimeMillis
|
||||
|
||||
@SuppressLint("UsableSpace")
|
||||
override fun availableDiskSpaceBytes(path: File) = path.usableSpace
|
||||
@@ -64,7 +59,7 @@ interface ProcessInfo {
|
||||
override fun availableRam(context: Context): AvailableRam {
|
||||
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
||||
|
||||
if (SDK_INT >= 19 && activityManager.isLowRamDevice) {
|
||||
if (activityManager.isLowRamDevice) {
|
||||
return LowRamDevice
|
||||
} else {
|
||||
activityManager.getMemoryInfo(memoryInfo)
|
||||
@@ -91,17 +86,7 @@ interface ProcessInfo {
|
||||
val myPid = Process.myPid()
|
||||
val ticksAtProcessStart = readProcessStartTicks(myPid)
|
||||
|
||||
val ticksPerSecond = if (SDK_INT >= 21) {
|
||||
Os.sysconf(OsConstants._SC_CLK_TCK)
|
||||
} else {
|
||||
val tckConstant = try {
|
||||
Class.forName("android.system.OsConstants").getField("_SC_CLK_TCK").getInt(null)
|
||||
} catch (e: ClassNotFoundException) {
|
||||
Class.forName("libcore.io.OsConstants").getField("_SC_CLK_TCK").getInt(null)
|
||||
}
|
||||
val os = Class.forName("libcore.io.Libcore").getField("os").get(null)!!
|
||||
os::class.java.getMethod("sysconf", Integer.TYPE).invoke(os, tckConstant) as Long
|
||||
}
|
||||
val ticksPerSecond = Os.sysconf(OsConstants._SC_CLK_TCK)
|
||||
return ticksAtProcessStart * 1000 / ticksPerSecond
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ android {
|
||||
compileSdk = libs.versions.androidCompileSdk.get().toInt()
|
||||
defaultConfig {
|
||||
targetSdk = libs.versions.androidCompileSdk.get().toInt()
|
||||
minSdk = 18
|
||||
minSdk = libs.versions.androidMinSdk.get().toInt()
|
||||
}
|
||||
buildFeatures.buildConfig = false
|
||||
namespace = "com.squareup.leakcanary.android.uiautomator"
|
||||
|
||||
@@ -32,22 +32,13 @@ class UiAutomatorShellHeapDumper(
|
||||
|
||||
// Based on https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:benchmark/benchmark-common/src/main/java/androidx/benchmark/Shell.kt;l=467;drc=8f2ba6a5469f67b7e385878d704f97bde22419ce
|
||||
private fun UiDevice.getPidsForProcess(processName: String): List<Int> {
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
return pgrepLF(pattern = processName)
|
||||
.mapNotNull { (pid, fullProcessName) ->
|
||||
if (fullProcessNameMatchesProcess(fullProcessName, processName)) {
|
||||
pid
|
||||
} else {
|
||||
null
|
||||
}
|
||||
return pgrepLF(pattern = processName)
|
||||
.mapNotNull { (pid, fullProcessName) ->
|
||||
if (fullProcessNameMatchesProcess(fullProcessName, processName)) {
|
||||
pid
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
val processList = executeShellCommand("ps")
|
||||
return processList.lines()
|
||||
.filter { psLineContainsProcess(it, processName) }
|
||||
.map {
|
||||
val columns = SPACE_PATTERN.split(it)
|
||||
columns[1].toInt()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,23 +52,12 @@ class UiAutomatorShellHeapDumper(
|
||||
}
|
||||
}
|
||||
|
||||
private fun psLineContainsProcess(
|
||||
psOutputLine: String,
|
||||
processName: String
|
||||
): Boolean {
|
||||
return psOutputLine.endsWith(" $processName") || psOutputLine.endsWith("/$processName")
|
||||
}
|
||||
|
||||
private fun fullProcessNameMatchesProcess(
|
||||
fullProcessName: String,
|
||||
processName: String
|
||||
): Boolean {
|
||||
return fullProcessName == processName || fullProcessName.endsWith("/$processName")
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private val SPACE_PATTERN = Regex("\\s+")
|
||||
}
|
||||
}
|
||||
|
||||
fun HeapDumper.Companion.forUiAutomatorAsShell(
|
||||
|
||||
@@ -20,8 +20,8 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "org.leakcanary"
|
||||
// 21 required by Compose
|
||||
minSdk = 21
|
||||
// 26 minimum SDK for modern Android features
|
||||
minSdk = 26
|
||||
targetSdk = libs.versions.androidCompileSdk.get().toInt()
|
||||
|
||||
buildConfigField("String", "GIT_SHA", "\"${gitSha()}\"")
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
public abstract class leakcanary/AndroidLeakFixes : java/lang/Enum {
|
||||
public static final field ACCESSIBILITY_NODE_INFO Lleakcanary/AndroidLeakFixes;
|
||||
public static final field ACTIVITY_MANAGER Lleakcanary/AndroidLeakFixes;
|
||||
public static final field BUBBLE_POPUP Lleakcanary/AndroidLeakFixes;
|
||||
public static final field CONNECTIVITY_MANAGER Lleakcanary/AndroidLeakFixes;
|
||||
public static final field Companion Lleakcanary/AndroidLeakFixes$Companion;
|
||||
public static final field FLUSH_HANDLER_THREADS Lleakcanary/AndroidLeakFixes;
|
||||
public static final field IMM_CUR_ROOT_VIEW Lleakcanary/AndroidLeakFixes;
|
||||
public static final field IMM_FOCUSED_VIEW Lleakcanary/AndroidLeakFixes;
|
||||
public static final field LAST_HOVERED_VIEW Lleakcanary/AndroidLeakFixes;
|
||||
public static final field MEDIA_SESSION_LEGACY_HELPER Lleakcanary/AndroidLeakFixes;
|
||||
public static final field PERMISSION_CONTROLLER_MANAGER Lleakcanary/AndroidLeakFixes;
|
||||
public static final field SAMSUNG_CLIPBOARD_MANAGER Lleakcanary/AndroidLeakFixes;
|
||||
public static final field SPELL_CHECKER Lleakcanary/AndroidLeakFixes;
|
||||
public static final field TEXT_LINE_POOL Lleakcanary/AndroidLeakFixes;
|
||||
public static final field USER_MANAGER Lleakcanary/AndroidLeakFixes;
|
||||
public static final field VIEW_LOCATION_HOLDER Lleakcanary/AndroidLeakFixes;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package leakcanary
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.annotation.TargetApi
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
@@ -9,7 +8,6 @@ import android.content.Context.INPUT_METHOD_SERVICE
|
||||
import android.content.ContextWrapper
|
||||
import android.os.Build.MANUFACTURER
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import android.os.Looper
|
||||
@@ -18,18 +16,12 @@ import android.view.View
|
||||
import android.view.Window
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.view.textservice.TextServicesManager
|
||||
import android.widget.TextView
|
||||
import curtains.Curtains
|
||||
import curtains.OnRootViewRemovedListener
|
||||
import java.lang.reflect.Array
|
||||
import java.lang.reflect.Field
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.lang.reflect.Method
|
||||
import java.lang.reflect.Modifier
|
||||
import java.lang.reflect.Proxy
|
||||
import java.util.EnumSet
|
||||
import leakcanary.internal.ReferenceCleaner
|
||||
import leakcanary.internal.friendly.checkMainThread
|
||||
import leakcanary.internal.friendly.noOpDelegate
|
||||
import shark.SharkLog
|
||||
@@ -40,30 +32,6 @@ import shark.SharkLog
|
||||
@SuppressLint("NewApi")
|
||||
enum class AndroidLeakFixes {
|
||||
|
||||
/**
|
||||
* MediaSessionLegacyHelper is a static singleton and did not use the application context.
|
||||
* Introduced in android-5.0.1_r1, fixed in Android 5.1.0_r1.
|
||||
* https://github.com/android/platform_frameworks_base/commit/9b5257c9c99c4cb541d8e8e78fb04f008b1a9091
|
||||
*
|
||||
* We fix this leak by invoking MediaSessionLegacyHelper.getHelper() early in the app lifecycle.
|
||||
*/
|
||||
MEDIA_SESSION_LEGACY_HELPER {
|
||||
override fun apply(application: Application) {
|
||||
if (SDK_INT != 21) {
|
||||
return
|
||||
}
|
||||
backgroundHandler.post {
|
||||
try {
|
||||
val clazz = Class.forName("android.media.session.MediaSessionLegacyHelper")
|
||||
val getHelperMethod = clazz.getDeclaredMethod("getHelper", Context::class.java)
|
||||
getHelperMethod.invoke(null, application)
|
||||
} catch (ignored: Exception) {
|
||||
SharkLog.d(ignored) { "Could not fix the $name leak" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* This flushes the TextLine pool when an activity is destroyed, to prevent memory leaks.
|
||||
*
|
||||
@@ -232,32 +200,6 @@ enum class AndroidLeakFixes {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* ConnectivityManager has a sInstance field that is set when the first ConnectivityManager instance is created.
|
||||
* ConnectivityManager has a mContext field.
|
||||
* When calling activity.getSystemService(Context.CONNECTIVITY_SERVICE) , the first ConnectivityManager instance
|
||||
* is created with the activity context and stored in sInstance.
|
||||
* That activity context then leaks forever.
|
||||
*
|
||||
* This fix makes sure the connectivity manager is created with the application context.
|
||||
*
|
||||
* Tracked here: https://code.google.com/p/android/issues/detail?id=198852
|
||||
* Introduced here: https://github.com/android/platform_frameworks_base/commit/e0bef71662d81caaaa0d7214fb0bef5d39996a69
|
||||
*/
|
||||
CONNECTIVITY_MANAGER {
|
||||
override fun apply(application: Application) {
|
||||
if (SDK_INT > 23) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
application.getSystemService(Context.CONNECTIVITY_SERVICE)
|
||||
} catch (ignored: Exception) {
|
||||
SharkLog.d(ignored) { "Could not fix the $name leak" }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* ClipboardUIManager is a static singleton that leaks an activity context.
|
||||
* This fix makes sure the manager is called with an application context.
|
||||
@@ -344,49 +286,6 @@ enum class AndroidLeakFixes {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Samsung added a static mContext field to ActivityManager, holding a reference to the activity.
|
||||
*
|
||||
* This fix clears the field when an activity is destroyed if it refers to this specific activity.
|
||||
*
|
||||
* Observed here: https://github.com/square/leakcanary/issues/177
|
||||
*/
|
||||
ACTIVITY_MANAGER {
|
||||
override fun apply(application: Application) {
|
||||
if (MANUFACTURER != SAMSUNG || SDK_INT != 22) {
|
||||
return
|
||||
}
|
||||
|
||||
backgroundHandler.post {
|
||||
val contextField: Field
|
||||
try {
|
||||
contextField = application
|
||||
.getSystemService(Context.ACTIVITY_SERVICE)
|
||||
.javaClass
|
||||
.getDeclaredField("mContext")
|
||||
contextField.isAccessible = true
|
||||
if ((contextField.modifiers or Modifier.STATIC) != contextField.modifiers) {
|
||||
SharkLog.d { "Could not fix the $name leak, contextField=$contextField" }
|
||||
return@post
|
||||
}
|
||||
} catch (ignored: Exception) {
|
||||
SharkLog.d(ignored) { "Could not fix the $name leak" }
|
||||
return@post
|
||||
}
|
||||
|
||||
application.onActivityDestroyed { activity ->
|
||||
try {
|
||||
if (contextField.get(null) == activity) {
|
||||
contextField.set(null, null)
|
||||
}
|
||||
} catch (ignored: Exception) {
|
||||
SharkLog.d(ignored) { "Could not fix the $name leak" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* In Android P, ViewLocationHolder has an mRoot field that is not cleared in its clear() method.
|
||||
* Introduced in https://github.com/aosp-mirror/platform_frameworks_base/commit/86b326012813f09d8f1de7d6d26c986a909d
|
||||
@@ -407,69 +306,6 @@ enum class AndroidLeakFixes {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Fix for https://code.google.com/p/android/issues/detail?id=171190 .
|
||||
*
|
||||
* When a view that has focus gets detached, we wait for the main thread to be idle and then
|
||||
* check if the InputMethodManager is leaking a view. If yes, we tell it that the decor view got
|
||||
* focus, which is what happens if you press home and come back from recent apps. This replaces
|
||||
* the reference to the detached view with a reference to the decor view.
|
||||
*/
|
||||
IMM_FOCUSED_VIEW {
|
||||
// mServedView should not be accessed on API 29+. Make this clear to Lint with the
|
||||
// TargetApi annotation.
|
||||
@TargetApi(23)
|
||||
@SuppressLint("PrivateApi")
|
||||
override fun apply(application: Application) {
|
||||
// Fixed in API 24.
|
||||
if (SDK_INT > 23) {
|
||||
return
|
||||
}
|
||||
val inputMethodManager =
|
||||
application.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
val mServedViewField: Field
|
||||
val mHField: Field
|
||||
val finishInputLockedMethod: Method
|
||||
val focusInMethod: Method
|
||||
try {
|
||||
mServedViewField =
|
||||
InputMethodManager::class.java.getDeclaredField("mServedView")
|
||||
mServedViewField.isAccessible = true
|
||||
mHField = InputMethodManager::class.java.getDeclaredField("mH")
|
||||
mHField.isAccessible = true
|
||||
finishInputLockedMethod =
|
||||
InputMethodManager::class.java.getDeclaredMethod("finishInputLocked")
|
||||
finishInputLockedMethod.isAccessible = true
|
||||
focusInMethod = InputMethodManager::class.java.getDeclaredMethod(
|
||||
"focusIn", View::class.java
|
||||
)
|
||||
focusInMethod.isAccessible = true
|
||||
} catch (ignored: Exception) {
|
||||
SharkLog.d(ignored) { "Could not fix the $name leak" }
|
||||
return
|
||||
}
|
||||
application.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks
|
||||
by noOpDelegate() {
|
||||
override fun onActivityCreated(
|
||||
activity: Activity,
|
||||
savedInstanceState: Bundle?
|
||||
) {
|
||||
activity.window.onDecorViewReady {
|
||||
val cleaner = ReferenceCleaner(
|
||||
inputMethodManager,
|
||||
mHField,
|
||||
mServedViewField,
|
||||
finishInputLockedMethod
|
||||
)
|
||||
val rootView = activity.window.decorView.rootView
|
||||
val viewTreeObserver = rootView.viewTreeObserver
|
||||
viewTreeObserver.addOnGlobalFocusChangeListener(cleaner)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* When an activity is destroyed, the corresponding ViewRootImpl instance is released and ready to
|
||||
* be garbage collected.
|
||||
@@ -560,155 +396,6 @@ enum class AndroidLeakFixes {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Every editable TextView has an Editor instance which has a SpellChecker instance. SpellChecker
|
||||
* is in charge of displaying the little squiggle spans that show typos. SpellChecker starts a
|
||||
* SpellCheckerSession as needed and then closes it when the TextView is detached from the window.
|
||||
* A SpellCheckerSession is in charge of communicating with the spell checker service (which lives
|
||||
* in another process) through TextServicesManager.
|
||||
*
|
||||
* The SpellChecker sends the TextView content to the spell checker service every 400ms, ie every
|
||||
* time the service calls back with a result the SpellChecker schedules another check for 400ms
|
||||
* later.
|
||||
*
|
||||
* When the TextView is detached from the window, the spell checker closes the session. In practice,
|
||||
* SpellCheckerSessionListenerImpl.mHandler is set to null and when the service calls
|
||||
* SpellCheckerSessionListenerImpl.onGetSuggestions or
|
||||
* SpellCheckerSessionListenerImpl.onGetSentenceSuggestions back from another process, there's a
|
||||
* null check for SpellCheckerSessionListenerImpl.mHandler and the callback is dropped.
|
||||
*
|
||||
* Unfortunately, on Android M there's a race condition in how that's done. When the service calls
|
||||
* back into our app process, the IPC call is received on a binder thread. That's when the null
|
||||
* check happens. If the session is not closed at this point (mHandler not null), the callback is
|
||||
* then posted to the main thread. If on the main thread the session is closed after that post but
|
||||
* prior to that post being handled, then the post will still be processed, after the session has
|
||||
* been closed.
|
||||
*
|
||||
* When the post is processed, SpellCheckerSession calls back into SpellChecker which in turns
|
||||
* schedules a new spell check to be ran in 400ms. The check is an anonymous inner class
|
||||
* (SpellChecker$1) stored as SpellChecker.mSpellRunnable and implementing Runnable. It is scheduled
|
||||
* by calling [View.postDelayed]. As we've seen, at this point the session may be closed which means
|
||||
* that the view has been detached. [View.postDelayed] behaves differently when a view is detached:
|
||||
* instead of posting to the single [Handler] used by the view hierarchy, it enqueues the Runnable
|
||||
* into ViewRootImpl.RunQueue, a static queue that holds on to "actions" to be executed. As soon as
|
||||
* a view hierarchy is attached, the ViewRootImpl.RunQueue is processed and emptied.
|
||||
*
|
||||
* Unfortunately, that means that as long as no view hierarchy is attached, ie as long as there
|
||||
* are no activities alive, the actions stay in ViewRootImpl.RunQueue. That means SpellChecker$1
|
||||
* ends up being kept in memory. It holds on to SpellChecker which in turns holds on
|
||||
* to the detached TextView and corresponding destroyed activity & view hierarchy.
|
||||
*
|
||||
* We have a fix for this! When the spell check session is closed, we replace
|
||||
* SpellCheckerSession.mSpellCheckerSessionListener (which normally is the SpellChecker) with a
|
||||
* no-op implementation. So even if callbacks are enqueued to the main thread handler, these
|
||||
* callbacks will call the no-op implementation and SpellChecker will not be scheduling a spell
|
||||
* check.
|
||||
*
|
||||
* Sources to corroborate:
|
||||
*
|
||||
* https://android.googlesource.com/platform/frameworks/base/+/marshmallow-release/core/java/android/view/textservice/SpellCheckerSession.java
|
||||
* https://android.googlesource.com/platform/frameworks/base/+/marshmallow-release/core/java/android/view/textservice/TextServicesManager.java
|
||||
* https://android.googlesource.com/platform/frameworks/base/+/marshmallow-release/core/java/android/widget/SpellChecker.java
|
||||
* https://android.googlesource.com/platform/frameworks/base/+/marshmallow-release/core/java/android/view/ViewRootImpl.java
|
||||
*/
|
||||
SPELL_CHECKER {
|
||||
@TargetApi(23)
|
||||
@SuppressLint("PrivateApi")
|
||||
override fun apply(application: Application) {
|
||||
if (SDK_INT != 23) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val textServiceClass = TextServicesManager::class.java
|
||||
val getInstanceMethod = textServiceClass.getDeclaredMethod("getInstance")
|
||||
|
||||
val sServiceField = textServiceClass.getDeclaredField("sService")
|
||||
sServiceField.isAccessible = true
|
||||
|
||||
val serviceStubInterface =
|
||||
Class.forName("com.android.internal.textservice.ITextServicesManager")
|
||||
|
||||
val spellCheckSessionClass = Class.forName("android.view.textservice.SpellCheckerSession")
|
||||
val mSpellCheckerSessionListenerField =
|
||||
spellCheckSessionClass.getDeclaredField("mSpellCheckerSessionListener")
|
||||
mSpellCheckerSessionListenerField.isAccessible = true
|
||||
|
||||
val spellCheckerSessionListenerImplClass =
|
||||
Class.forName(
|
||||
"android.view.textservice.SpellCheckerSession\$SpellCheckerSessionListenerImpl"
|
||||
)
|
||||
val listenerImplHandlerField =
|
||||
spellCheckerSessionListenerImplClass.getDeclaredField("mHandler")
|
||||
listenerImplHandlerField.isAccessible = true
|
||||
|
||||
val spellCheckSessionHandlerClass =
|
||||
Class.forName("android.view.textservice.SpellCheckerSession\$1")
|
||||
val outerInstanceField = spellCheckSessionHandlerClass.getDeclaredField("this$0")
|
||||
outerInstanceField.isAccessible = true
|
||||
|
||||
val listenerInterface =
|
||||
Class.forName("android.view.textservice.SpellCheckerSession\$SpellCheckerSessionListener")
|
||||
val noOpListener = Proxy.newProxyInstance(
|
||||
listenerInterface.classLoader, arrayOf(listenerInterface)
|
||||
) { _: Any, _: Method, _: kotlin.Array<Any>? ->
|
||||
SharkLog.d { "Received call to no-op SpellCheckerSessionListener after session closed" }
|
||||
}
|
||||
|
||||
// Ensure a TextServicesManager instance is created and TextServicesManager.sService set.
|
||||
getInstanceMethod
|
||||
.invoke(null)
|
||||
val realService = sServiceField[null]!!
|
||||
|
||||
val spellCheckerListenerToSession = mutableMapOf<Any, Any>()
|
||||
|
||||
val proxyService = Proxy.newProxyInstance(
|
||||
serviceStubInterface.classLoader, arrayOf(serviceStubInterface)
|
||||
) { _: Any, method: Method, args: kotlin.Array<Any>? ->
|
||||
try {
|
||||
if (method.name == "getSpellCheckerService") {
|
||||
// getSpellCheckerService is called when the session is opened, which allows us to
|
||||
// capture the corresponding SpellCheckerSession instance via
|
||||
// SpellCheckerSessionListenerImpl.mHandler.this$0
|
||||
val spellCheckerSessionListener = args!![3]
|
||||
val handler = listenerImplHandlerField[spellCheckerSessionListener]!!
|
||||
val spellCheckerSession = outerInstanceField[handler]!!
|
||||
// We add to a map of SpellCheckerSessionListenerImpl to SpellCheckerSession
|
||||
spellCheckerListenerToSession[spellCheckerSessionListener] = spellCheckerSession
|
||||
} else if (method.name == "finishSpellCheckerService") {
|
||||
// finishSpellCheckerService is called when the session is open. After the session has been
|
||||
// closed, any pending work posted to SpellCheckerSession.mHandler should be ignored. We do
|
||||
// so by replacing mSpellCheckerSessionListener with a no-op implementation.
|
||||
val spellCheckerSessionListener = args!![0]
|
||||
val spellCheckerSession =
|
||||
spellCheckerListenerToSession.remove(spellCheckerSessionListener)!!
|
||||
// We use the SpellCheckerSessionListenerImpl to find the corresponding SpellCheckerSession
|
||||
// At this point in time the session was just closed to
|
||||
// SpellCheckerSessionListenerImpl.mHandler is null, which is why we had to capture
|
||||
// the SpellCheckerSession during the getSpellCheckerService call.
|
||||
mSpellCheckerSessionListenerField[spellCheckerSession] = noOpListener
|
||||
}
|
||||
} catch (ignored: Exception) {
|
||||
SharkLog.d(ignored) { "Unable to fix SpellChecker leak" }
|
||||
}
|
||||
// Standard delegation
|
||||
try {
|
||||
return@newProxyInstance if (args != null) {
|
||||
method.invoke(realService, *args)
|
||||
} else {
|
||||
method.invoke(realService)
|
||||
}
|
||||
} catch (invocationException: InvocationTargetException) {
|
||||
throw invocationException.targetException
|
||||
}
|
||||
}
|
||||
sServiceField[null] = proxyService
|
||||
} catch (ignored: Exception) {
|
||||
SharkLog.d(ignored) { "Unable to fix SpellChecker leak" }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* PermissionControllerManager stores the first context it's initialized with forever.
|
||||
* Sometimes it's an Activity context which then leaks after Activity is destroyed.
|
||||
|
||||
@@ -105,7 +105,6 @@ class FixedWindowCallback implements Window.Callback {
|
||||
return delegate.onSearchRequested();
|
||||
}
|
||||
|
||||
@RequiresApi(23)
|
||||
@Override public boolean onSearchRequested(SearchEvent searchEvent) {
|
||||
return delegate.onSearchRequested(searchEvent);
|
||||
}
|
||||
@@ -114,7 +113,7 @@ class FixedWindowCallback implements Window.Callback {
|
||||
return delegate.onWindowStartingActionMode(callback);
|
||||
}
|
||||
|
||||
@RequiresApi(23) @Nullable @Override
|
||||
@Nullable @Override
|
||||
public ActionMode onWindowStartingActionMode(ActionMode.Callback callback,
|
||||
int type) {
|
||||
return delegate.onWindowStartingActionMode(callback, type);
|
||||
@@ -128,13 +127,11 @@ class FixedWindowCallback implements Window.Callback {
|
||||
delegate.onActionModeFinished(mode);
|
||||
}
|
||||
|
||||
@RequiresApi(24)
|
||||
@Override public void onProvideKeyboardShortcuts(List<KeyboardShortcutGroup> data,
|
||||
@Nullable Menu menu, int deviceId) {
|
||||
delegate.onProvideKeyboardShortcuts(data, menu, deviceId);
|
||||
}
|
||||
|
||||
@RequiresApi(26)
|
||||
@Override public void onPointerCaptureChanged(boolean hasCapture) {
|
||||
delegate.onPointerCaptureChanged(hasCapture);
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.example.leakcanary"
|
||||
minSdk = 16
|
||||
minSdk = libs.versions.androidMinSdk.get().toInt()
|
||||
targetSdk = libs.versions.androidCompileSdk.get().toInt()
|
||||
|
||||
versionCode = 1
|
||||
|
||||
Reference in New Issue
Block a user