<lambda>null1 package leakcanary.internal
2
3 import android.app.Activity
4 import android.app.Application
5 import android.app.Application.ActivityLifecycleCallbacks
6 import android.app.UiModeManager
7 import android.content.ComponentName
8 import android.content.Context
9 import android.content.Context.UI_MODE_SERVICE
10 import android.content.Intent
11 import android.content.pm.ApplicationInfo
12 import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED
13 import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED
14 import android.content.pm.PackageManager.DONT_KILL_APP
15 import android.content.pm.ShortcutInfo.Builder
16 import android.content.pm.ShortcutManager
17 import android.content.res.Configuration
18 import android.graphics.drawable.Icon
19 import android.os.Build.VERSION
20 import android.os.Build.VERSION_CODES
21 import android.os.Handler
22 import android.os.HandlerThread
23 import com.squareup.leakcanary.core.BuildConfig
24 import com.squareup.leakcanary.core.R
25 import leakcanary.AppWatcher
26 import leakcanary.EventListener.Event
27 import leakcanary.GcTrigger
28 import leakcanary.LeakCanary
29 import leakcanary.OnObjectRetainedListener
30 import leakcanary.internal.HeapDumpControl.ICanHazHeap.Nope
31 import leakcanary.internal.HeapDumpControl.ICanHazHeap.Yup
32 import leakcanary.internal.InternalLeakCanary.FormFactor.MOBILE
33 import leakcanary.internal.InternalLeakCanary.FormFactor.TV
34 import leakcanary.internal.InternalLeakCanary.FormFactor.WATCH
35 import leakcanary.internal.friendly.mainHandler
36 import leakcanary.internal.friendly.noOpDelegate
37 import leakcanary.internal.tv.TvOnRetainInstanceListener
38 import shark.SharkLog
39
40 internal object InternalLeakCanary : (Application) -> Unit, OnObjectRetainedListener {
41
42 private const val DYNAMIC_SHORTCUT_ID = "com.squareup.leakcanary.dynamic_shortcut"
43
44 private lateinit var heapDumpTrigger: HeapDumpTrigger
45
46 // You're wrong https://discuss.kotlinlang.org/t/object-or-top-level-property-name-warning/6621/7
47 @Suppress("ObjectPropertyName")
48 private var _application: Application? = null
49
50 val application: Application
51 get() {
52 check(_application != null) {
53 "LeakCanary not installed, see AppWatcher.manualInstall()"
54 }
55 return _application!!
56 }
57
58 // BuildConfig.LIBRARY_VERSION is stripped so this static var is how we keep it around to find
59 // it later when parsing the heap dump.
60 @Suppress("unused")
61 @JvmStatic
62 private var version = BuildConfig.LIBRARY_VERSION
63
64 @Volatile
65 var applicationVisible = false
66 private set
67
68 private val isDebuggableBuild by lazy {
69 (application.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0
70 }
71
72 fun createLeakDirectoryProvider(context: Context): LeakDirectoryProvider {
73 val appContext = context.applicationContext
74 return LeakDirectoryProvider(appContext, {
75 LeakCanary.config.maxStoredHeapDumps
76 }, {
77 LeakCanary.config.requestWriteExternalStoragePermission
78 })
79 }
80
81 internal enum class FormFactor {
82 MOBILE,
83 TV,
84 WATCH,
85 }
86
87 val formFactor by lazy {
88 return@lazy when ((application.getSystemService(UI_MODE_SERVICE) as UiModeManager).currentModeType) {
89 Configuration.UI_MODE_TYPE_TELEVISION -> TV
90 Configuration.UI_MODE_TYPE_WATCH -> WATCH
91 else -> MOBILE
92 }
93 }
94
95 val isInstantApp by lazy {
96 VERSION.SDK_INT >= VERSION_CODES.O && application.packageManager.isInstantApp
97 }
98
99 val onRetainInstanceListener by lazy {
100 when (formFactor) {
101 TV -> TvOnRetainInstanceListener(application)
102 else -> DefaultOnRetainInstanceListener()
103 }
104 }
105
106 var resumedActivity: Activity? = null
107
108 private val heapDumpPrefs by lazy {
109 application.getSharedPreferences("LeakCanaryHeapDumpPrefs", Context.MODE_PRIVATE)
110 }
111
112 internal var dumpEnabledInAboutScreen: Boolean
113 get() {
114 return heapDumpPrefs
115 .getBoolean("AboutScreenDumpEnabled", true)
116 }
117 set(value) {
118 heapDumpPrefs
119 .edit()
120 .putBoolean("AboutScreenDumpEnabled", value)
121 .apply()
122 }
123
124 override fun invoke(application: Application) {
125 _application = application
126
127 checkRunningInDebuggableBuild()
128
129 AppWatcher.objectWatcher.addOnObjectRetainedListener(this)
130
131 val gcTrigger = GcTrigger.Default
132
133 val configProvider = { LeakCanary.config }
134
135 val handlerThread = HandlerThread(LEAK_CANARY_THREAD_NAME)
136 handlerThread.start()
137 val backgroundHandler = Handler(handlerThread.looper)
138
139 heapDumpTrigger = HeapDumpTrigger(
140 application, backgroundHandler, AppWatcher.objectWatcher, gcTrigger,
141 configProvider
142 )
143 application.registerVisibilityListener { applicationVisible ->
144 this.applicationVisible = applicationVisible
145 heapDumpTrigger.onApplicationVisibilityChanged(applicationVisible)
146 }
147 registerResumedActivityListener(application)
148 addDynamicShortcut(application)
149
150 // We post so that the log happens after Application.onCreate()
151 mainHandler.post {
152 // https://github.com/square/leakcanary/issues/1981
153 // We post to a background handler because HeapDumpControl.iCanHasHeap() checks a shared pref
154 // which blocks until loaded and that creates a StrictMode violation.
155 backgroundHandler.post {
156 SharkLog.d {
157 when (val iCanHasHeap = HeapDumpControl.iCanHasHeap()) {
158 is Yup -> application.getString(R.string.leak_canary_heap_dump_enabled_text)
159 is Nope -> application.getString(
160 R.string.leak_canary_heap_dump_disabled_text, iCanHasHeap.reason()
161 )
162 }
163 }
164 }
165 }
166 }
167
168 private fun checkRunningInDebuggableBuild() {
169 if (isDebuggableBuild) {
170 return
171 }
172
173 if (!application.resources.getBoolean(R.bool.leak_canary_allow_in_non_debuggable_build)) {
174 throw Error(
175 """
176 LeakCanary in non-debuggable build
177
178 LeakCanary should only be used in debug builds, but this APK is not debuggable.
179 Please follow the instructions on the "Getting started" page to only include LeakCanary in
180 debug builds: https://square.github.io/leakcanary/getting_started/
181
182 If you're sure you want to include LeakCanary in a non-debuggable build, follow the
183 instructions here: https://square.github.io/leakcanary/recipes/#leakcanary-in-release-builds
184 """.trimIndent()
185 )
186 }
187 }
188
189 private fun registerResumedActivityListener(application: Application) {
190 application.registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks by noOpDelegate() {
191 override fun onActivityResumed(activity: Activity) {
192 resumedActivity = activity
193 }
194
195 override fun onActivityPaused(activity: Activity) {
196 if (resumedActivity === activity) {
197 resumedActivity = null
198 }
199 }
200 })
201 }
202
203 @Suppress("ReturnCount")
204 private fun addDynamicShortcut(application: Application) {
205 if (VERSION.SDK_INT < VERSION_CODES.N_MR1) {
206 return
207 }
208 if (!application.resources.getBoolean(R.bool.leak_canary_add_dynamic_shortcut)) {
209 return
210 }
211 if (isInstantApp) {
212 // Instant Apps don't have access to ShortcutManager
213 return
214 }
215 val shortcutManager = application.getSystemService(ShortcutManager::class.java)
216 if (shortcutManager == null) {
217 // https://github.com/square/leakcanary/issues/2430
218 // ShortcutManager null on Android TV
219 return
220 }
221 val dynamicShortcuts = shortcutManager.dynamicShortcuts
222
223 val shortcutInstalled =
224 dynamicShortcuts.any { shortcut -> shortcut.id == DYNAMIC_SHORTCUT_ID }
225
226 if (shortcutInstalled) {
227 return
228 }
229
230 val mainIntent = Intent(Intent.ACTION_MAIN, null)
231 mainIntent.addCategory(Intent.CATEGORY_LAUNCHER)
232 mainIntent.setPackage(application.packageName)
233 val activities = application.packageManager.queryIntentActivities(mainIntent, 0)
234 .filter {
235 it.activityInfo.name != "leakcanary.internal.activity.LeakLauncherActivity"
236 }
237
238 if (activities.isEmpty()) {
239 return
240 }
241
242 val firstMainActivity = activities.first()
243 .activityInfo
244
245 // Displayed on long tap on app icon
246 val longLabel: String
247 // Label when dropping shortcut to launcher
248 val shortLabel: String
249
250 val leakActivityLabel = application.getString(R.string.leak_canary_shortcut_label)
251
252 if (activities.isEmpty()) {
253 longLabel = leakActivityLabel
254 shortLabel = leakActivityLabel
255 } else {
256 val firstLauncherActivityLabel = if (firstMainActivity.labelRes != 0) {
257 application.getString(firstMainActivity.labelRes)
258 } else {
259 application.packageManager.getApplicationLabel(application.applicationInfo)
260 }
261 val fullLengthLabel = "$firstLauncherActivityLabel $leakActivityLabel"
262 // short label should be under 10 and long label under 25
263 if (fullLengthLabel.length > 10) {
264 if (fullLengthLabel.length <= 25) {
265 longLabel = fullLengthLabel
266 shortLabel = leakActivityLabel
267 } else {
268 longLabel = leakActivityLabel
269 shortLabel = leakActivityLabel
270 }
271 } else {
272 longLabel = fullLengthLabel
273 shortLabel = fullLengthLabel
274 }
275 }
276
277 val componentName = ComponentName(firstMainActivity.packageName, firstMainActivity.name)
278
279 val shortcutCount = dynamicShortcuts.count { shortcutInfo ->
280 shortcutInfo.activity == componentName
281 } + shortcutManager.manifestShortcuts.count { shortcutInfo ->
282 shortcutInfo.activity == componentName
283 }
284
285 if (shortcutCount >= shortcutManager.maxShortcutCountPerActivity) {
286 return
287 }
288
289 val intent = LeakCanary.newLeakDisplayActivityIntent()
290 intent.action = "Dummy Action because Android is stupid"
291 val shortcut = Builder(application, DYNAMIC_SHORTCUT_ID)
292 .setLongLabel(longLabel)
293 .setShortLabel(shortLabel)
294 .setActivity(componentName)
295 .setIcon(Icon.createWithResource(application, R.mipmap.leak_canary_icon))
296 .setIntent(intent)
297 .build()
298
299 try {
300 shortcutManager.addDynamicShortcuts(listOf(shortcut))
301 } catch (ignored: Throwable) {
302 SharkLog.d(ignored) {
303 "Could not add dynamic shortcut. " +
304 "shortcutCount=$shortcutCount, " +
305 "maxShortcutCountPerActivity=${shortcutManager.maxShortcutCountPerActivity}"
306 }
307 }
308 }
309
310 override fun onObjectRetained() = scheduleRetainedObjectCheck()
311
312 fun scheduleRetainedObjectCheck() {
313 if (this::heapDumpTrigger.isInitialized) {
314 heapDumpTrigger.scheduleRetainedObjectCheck()
315 }
316 }
317
318 fun onDumpHeapReceived(forceDump: Boolean) {
319 if (this::heapDumpTrigger.isInitialized) {
320 heapDumpTrigger.onDumpHeapReceived(forceDump)
321 }
322 }
323
324 fun setEnabledBlocking(
325 componentClassName: String,
326 enabled: Boolean
327 ) {
328 val component = ComponentName(application, componentClassName)
329 val newState =
330 if (enabled) COMPONENT_ENABLED_STATE_ENABLED else COMPONENT_ENABLED_STATE_DISABLED
331 // Blocks on IPC.
332 application.packageManager.setComponentEnabledSetting(component, newState, DONT_KILL_APP)
333 }
334
335 fun sendEvent(event: Event) {
336 for(listener in LeakCanary.config.eventListeners) {
337 listener.onEvent(event)
338 }
339 }
340
341 private const val LEAK_CANARY_THREAD_NAME = "LeakCanary-Heap-Dump"
342 }
343