<lambda>null1 package leakcanary
2
3 import android.annotation.SuppressLint
4 import android.annotation.TargetApi
5 import android.app.Activity
6 import android.app.Application
7 import android.content.Context
8 import android.content.Context.INPUT_METHOD_SERVICE
9 import android.content.ContextWrapper
10 import android.os.Build.MANUFACTURER
11 import android.os.Build.VERSION.SDK_INT
12 import android.os.Bundle
13 import android.os.Handler
14 import android.os.HandlerThread
15 import android.os.Looper
16 import android.os.UserManager
17 import android.view.View
18 import android.view.Window
19 import android.view.accessibility.AccessibilityNodeInfo
20 import android.view.inputmethod.InputMethodManager
21 import android.view.textservice.TextServicesManager
22 import android.widget.TextView
23 import curtains.Curtains
24 import curtains.OnRootViewRemovedListener
25 import java.lang.reflect.Array
26 import java.lang.reflect.Field
27 import java.lang.reflect.InvocationTargetException
28 import java.lang.reflect.Method
29 import java.lang.reflect.Modifier
30 import java.lang.reflect.Proxy
31 import java.util.EnumSet
32 import leakcanary.internal.ReferenceCleaner
33 import leakcanary.internal.friendly.checkMainThread
34 import leakcanary.internal.friendly.noOpDelegate
35 import shark.SharkLog
36
37 /**
38 * A collection of hacks to fix leaks in the Android Framework and other Google Android libraries.
39 */
40 @SuppressLint("NewApi")
41 enum class AndroidLeakFixes {
42
43 /**
44 * MediaSessionLegacyHelper is a static singleton and did not use the application context.
45 * Introduced in android-5.0.1_r1, fixed in Android 5.1.0_r1.
46 * https://github.com/android/platform_frameworks_base/commit/9b5257c9c99c4cb541d8e8e78fb04f008b1a9091
47 *
48 * We fix this leak by invoking MediaSessionLegacyHelper.getHelper() early in the app lifecycle.
49 */
50 MEDIA_SESSION_LEGACY_HELPER {
51 override fun apply(application: Application) {
52 if (SDK_INT != 21) {
53 return
54 }
55 backgroundHandler.post {
56 try {
57 val clazz = Class.forName("android.media.session.MediaSessionLegacyHelper")
58 val getHelperMethod = clazz.getDeclaredMethod("getHelper", Context::class.java)
59 getHelperMethod.invoke(null, application)
60 } catch (ignored: Exception) {
61 SharkLog.d(ignored) { "Could not fix the $name leak" }
62 }
63 }
64 }
65 },
66
67 /**
68 * This flushes the TextLine pool when an activity is destroyed, to prevent memory leaks.
69 *
70 * The first memory leak has been fixed in android-5.1.0_r1
71 * https://github.com/android/platform_frameworks_base/commit/893d6fe48d37f71e683f722457bea646994a10bf
72 *
73 * Second memory leak: https://github.com/android/platform_frameworks_base/commit/b3a9bc038d3a218b1dbdf7b5668e3d6c12be5ee4
74 */
75 TEXT_LINE_POOL {
76 override fun apply(application: Application) {
77 // Can't use reflection starting in SDK 28
78 if (SDK_INT >= 28) {
79 return
80 }
81 backgroundHandler.post {
82 try {
83 val textLineClass = Class.forName("android.text.TextLine")
84 val sCachedField = textLineClass.getDeclaredField("sCached")
85 sCachedField.isAccessible = true
86 // One time retrieval to make sure this will work.
87 val sCached = sCachedField.get(null)
88 // Can't happen in current Android source, but hidden APIs can change.
89 if (sCached == null || !sCached.javaClass.isArray) {
90 SharkLog.d { "Could not fix the $name leak, sCached=$sCached" }
91 return@post
92 }
93 application.onActivityDestroyed {
94 // Pool of TextLine instances.
95 val sCached = sCachedField.get(null)
96 // TextLine locks on sCached. We take that lock and clear the whole array at once.
97 synchronized(sCached) {
98 val length = Array.getLength(sCached)
99 for (i in 0 until length) {
100 Array.set(sCached, i, null)
101 }
102 }
103 }
104 } catch (ignored: Exception) {
105 SharkLog.d(ignored) { "Could not fix the $name leak" }
106 return@post
107 }
108 }
109 }
110 },
111
112 /**
113 * Obtaining the UserManager service ends up calling the hidden UserManager.get() method which
114 * stores the context in a singleton UserManager instance and then stores that instance in a
115 * static field.
116 *
117 * We obtain the user manager from an activity context, so if it hasn't been created yet it will
118 * leak that activity forever.
119 *
120 * This fix makes sure the UserManager is created and holds on to the Application context.
121 *
122 * Issue: https://code.google.com/p/android/issues/detail?id=173789
123 *
124 * Fixed in https://android.googlesource.com/platform/frameworks/base/+/5200e1cb07190a1f6874d72a4561064cad3ee3e0%5E%21/#F0
125 * (Android O)
126 */
127 USER_MANAGER {
128 @SuppressLint("NewApi")
129 override fun apply(application: Application) {
130 if (SDK_INT !in 17..25) {
131 return
132 }
133 try {
134 val getMethod = UserManager::class.java.getDeclaredMethod("get", Context::class.java)
135 getMethod.invoke(null, application)
136 } catch (ignored: Exception) {
137 SharkLog.d(ignored) { "Could not fix the $name leak" }
138 }
139 }
140 },
141
142 /**
143 * HandlerThread instances keep local reference to their last handled message after recycling it.
144 * That message is obtained by a dialog which sets on an OnClickListener on it and then never
145 * recycles it, expecting it to be garbage collected but it ends up being held by the
146 * HandlerThread.
147 */
148 FLUSH_HANDLER_THREADS {
149 override fun apply(application: Application) {
150 if (SDK_INT >= 31) {
151 return
152 }
153 val flushedThreadIds = mutableSetOf<Int>()
154 // Don't flush the backgroundHandler's thread, we're rescheduling all the time anyway.
155 flushedThreadIds += (backgroundHandler.looper.thread as HandlerThread).threadId
156 // Wait 2 seconds then look for handler threads every 3 seconds.
157 val flushNewHandlerThread = object : Runnable {
158 override fun run() {
159 val newHandlerThreadsById = findAllHandlerThreads()
160 .mapNotNull { thread ->
161 val threadId = thread.threadId
162 if (threadId == -1 || threadId in flushedThreadIds) {
163 null
164 } else {
165 threadId to thread
166 }
167 }
168 newHandlerThreadsById
169 .forEach { (threadId, handlerThread) ->
170 val looper = handlerThread.looper
171 if (looper == null) {
172 SharkLog.d { "Handler thread found without a looper: $handlerThread" }
173 return@forEach
174 }
175 flushedThreadIds += threadId
176 SharkLog.d { "Setting up flushing for $handlerThread" }
177 var scheduleFlush = true
178 val flushHandler = Handler(looper)
179 flushHandler.onEachIdle {
180 if (handlerThread.isAlive && scheduleFlush) {
181 scheduleFlush = false
182 // When the Handler thread becomes idle, we post a message to force it to move.
183 // Source: https://developer.squareup.com/blog/a-small-leak-will-sink-a-great-ship/
184 try {
185 val posted = flushHandler.postDelayed({
186 // Right after this postDelayed executes, the idle handler will likely be called
187 // again (if the queue is otherwise empty), so we'll need to schedule a flush
188 // again.
189 scheduleFlush = true
190 }, 1000)
191 if (!posted) {
192 SharkLog.d { "Failed to post to ${handlerThread.name}" }
193 }
194 } catch (ignored: RuntimeException) {
195 // If the thread is quitting, posting to it may throw. There is no safe and atomic way
196 // to check if a thread is quitting first then post it it.
197 SharkLog.d(ignored) { "Failed to post to ${handlerThread.name}" }
198 }
199 }
200 }
201 }
202 backgroundHandler.postDelayed(this, 3000)
203 }
204 }
205 backgroundHandler.postDelayed(flushNewHandlerThread, 2000)
206 }
207 },
208
209 /**
210 * Until API 28, AccessibilityNodeInfo has a mOriginalText field that was not properly cleared
211 * when instance were put back in the pool.
212 * Leak introduced here: https://android.googlesource.com/platform/frameworks/base/+/193520e3dff5248ddcf8435203bf99d2ba667219%5E%21/core/java/android/view/accessibility/AccessibilityNodeInfo.java
213 *
214 * Fixed here: https://android.googlesource.com/platform/frameworks/base/+/6f8ec1fd8c159b09d617ed6d9132658051443c0c
215 */
216 ACCESSIBILITY_NODE_INFO {
217 override fun apply(application: Application) {
218 if (SDK_INT >= 28) {
219 return
220 }
221
222 val starvePool = object : Runnable {
223 override fun run() {
224 val maxPoolSize = 50
225 for (i in 0 until maxPoolSize) {
226 AccessibilityNodeInfo.obtain()
227 }
228 backgroundHandler.postDelayed(this, 5000)
229 }
230 }
231 backgroundHandler.postDelayed(starvePool, 5000)
232 }
233 },
234
235 /**
236 * ConnectivityManager has a sInstance field that is set when the first ConnectivityManager instance is created.
237 * ConnectivityManager has a mContext field.
238 * When calling activity.getSystemService(Context.CONNECTIVITY_SERVICE) , the first ConnectivityManager instance
239 * is created with the activity context and stored in sInstance.
240 * That activity context then leaks forever.
241 *
242 * This fix makes sure the connectivity manager is created with the application context.
243 *
244 * Tracked here: https://code.google.com/p/android/issues/detail?id=198852
245 * Introduced here: https://github.com/android/platform_frameworks_base/commit/e0bef71662d81caaaa0d7214fb0bef5d39996a69
246 */
247 CONNECTIVITY_MANAGER {
248 override fun apply(application: Application) {
249 if (SDK_INT > 23) {
250 return
251 }
252
253 try {
254 application.getSystemService(Context.CONNECTIVITY_SERVICE)
255 } catch (ignored: Exception) {
256 SharkLog.d(ignored) { "Could not fix the $name leak" }
257 }
258 }
259 },
260
261 /**
262 * ClipboardUIManager is a static singleton that leaks an activity context.
263 * This fix makes sure the manager is called with an application context.
264 */
265 SAMSUNG_CLIPBOARD_MANAGER {
266 override fun apply(application: Application) {
267 if (MANUFACTURER != SAMSUNG || SDK_INT !in 19..21) {
268 return
269 }
270
271 try {
272 val managerClass = Class.forName("android.sec.clipboard.ClipboardUIManager")
273 val instanceMethod = managerClass.getDeclaredMethod("getInstance", Context::class.java)
274 instanceMethod.isAccessible = true
275 instanceMethod.invoke(null, application)
276 } catch (ignored: Exception) {
277 SharkLog.d(ignored) { "Could not fix the $name leak" }
278 }
279 }
280 },
281
282 /**
283 * A static helper for EditText bubble popups leaks a reference to the latest focused view.
284 *
285 * This fix clears it when the activity is destroyed.
286 */
287 BUBBLE_POPUP {
288 override fun apply(application: Application) {
289 if (MANUFACTURER != LG || SDK_INT !in 19..21) {
290 return
291 }
292
293 backgroundHandler.post {
294 val helperField: Field
295 try {
296 val helperClass = Class.forName("android.widget.BubblePopupHelper")
297 helperField = helperClass.getDeclaredField("sHelper")
298 helperField.isAccessible = true
299 } catch (ignored: Exception) {
300 SharkLog.d(ignored) { "Could not fix the $name leak" }
301 return@post
302 }
303
304 application.onActivityDestroyed {
305 try {
306 helperField.set(null, null)
307 } catch (ignored: Exception) {
308 SharkLog.d(ignored) { "Could not fix the $name leak" }
309 }
310 }
311 }
312 }
313 },
314
315 /**
316 * mLastHoveredView is a static field in TextView that leaks the last hovered view.
317 *
318 * This fix clears it when the activity is destroyed.
319 */
320 LAST_HOVERED_VIEW {
321 override fun apply(application: Application) {
322 if (MANUFACTURER != SAMSUNG || SDK_INT !in 19..21) {
323 return
324 }
325
326 backgroundHandler.post {
327 val field: Field
328 try {
329 field = TextView::class.java.getDeclaredField("mLastHoveredView")
330 field.isAccessible = true
331 } catch (ignored: Exception) {
332 SharkLog.d(ignored) { "Could not fix the $name leak" }
333 return@post
334 }
335
336 application.onActivityDestroyed {
337 try {
338 field.set(null, null)
339 } catch (ignored: Exception) {
340 SharkLog.d(ignored) { "Could not fix the $name leak" }
341 }
342 }
343 }
344 }
345 },
346
347 /**
348 * Samsung added a static mContext field to ActivityManager, holding a reference to the activity.
349 *
350 * This fix clears the field when an activity is destroyed if it refers to this specific activity.
351 *
352 * Observed here: https://github.com/square/leakcanary/issues/177
353 */
354 ACTIVITY_MANAGER {
355 override fun apply(application: Application) {
356 if (MANUFACTURER != SAMSUNG || SDK_INT != 22) {
357 return
358 }
359
360 backgroundHandler.post {
361 val contextField: Field
362 try {
363 contextField = application
364 .getSystemService(Context.ACTIVITY_SERVICE)
365 .javaClass
366 .getDeclaredField("mContext")
367 contextField.isAccessible = true
368 if ((contextField.modifiers or Modifier.STATIC) != contextField.modifiers) {
369 SharkLog.d { "Could not fix the $name leak, contextField=$contextField" }
370 return@post
371 }
372 } catch (ignored: Exception) {
373 SharkLog.d(ignored) { "Could not fix the $name leak" }
374 return@post
375 }
376
377 application.onActivityDestroyed { activity ->
378 try {
379 if (contextField.get(null) == activity) {
380 contextField.set(null, null)
381 }
382 } catch (ignored: Exception) {
383 SharkLog.d(ignored) { "Could not fix the $name leak" }
384 }
385 }
386 }
387 }
388 },
389
390 /**
391 * In Android P, ViewLocationHolder has an mRoot field that is not cleared in its clear() method.
392 * Introduced in https://github.com/aosp-mirror/platform_frameworks_base/commit/86b326012813f09d8f1de7d6d26c986a909d
393 *
394 * This leaks triggers very often when accessibility is on. To fix this leak we need to clear
395 * the ViewGroup.ViewLocationHolder.sPool pool. Unfortunately Android P prevents accessing that
396 * field through reflection. So instead, we call [ViewGroup#addChildrenForAccessibility] with
397 * a view group that has 32 children (32 being the pool size), which as result fills in the pool
398 * with 32 dumb views that reference a dummy context instead of an activity context.
399 *
400 * This fix empties the pool on every activity destroy and every AndroidX fragment view destroy.
401 * You can support other cases where views get detached by calling directly
402 * [ViewLocationHolderLeakFix.clearStaticPool].
403 */
404 VIEW_LOCATION_HOLDER {
405 override fun apply(application: Application) {
406 ViewLocationHolderLeakFix.applyFix(application)
407 }
408 },
409
410 /**
411 * Fix for https://code.google.com/p/android/issues/detail?id=171190 .
412 *
413 * When a view that has focus gets detached, we wait for the main thread to be idle and then
414 * check if the InputMethodManager is leaking a view. If yes, we tell it that the decor view got
415 * focus, which is what happens if you press home and come back from recent apps. This replaces
416 * the reference to the detached view with a reference to the decor view.
417 */
418 IMM_FOCUSED_VIEW {
419 // mServedView should not be accessed on API 29+. Make this clear to Lint with the
420 // TargetApi annotation.
421 @TargetApi(23)
422 @SuppressLint("PrivateApi")
423 override fun apply(application: Application) {
424 // Fixed in API 24.
425 if (SDK_INT > 23) {
426 return
427 }
428 val inputMethodManager =
429 application.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
430 val mServedViewField: Field
431 val mHField: Field
432 val finishInputLockedMethod: Method
433 val focusInMethod: Method
434 try {
435 mServedViewField =
436 InputMethodManager::class.java.getDeclaredField("mServedView")
437 mServedViewField.isAccessible = true
438 mHField = InputMethodManager::class.java.getDeclaredField("mH")
439 mHField.isAccessible = true
440 finishInputLockedMethod =
441 InputMethodManager::class.java.getDeclaredMethod("finishInputLocked")
442 finishInputLockedMethod.isAccessible = true
443 focusInMethod = InputMethodManager::class.java.getDeclaredMethod(
444 "focusIn", View::class.java
445 )
446 focusInMethod.isAccessible = true
447 } catch (ignored: Exception) {
448 SharkLog.d(ignored) { "Could not fix the $name leak" }
449 return
450 }
451 application.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks
452 by noOpDelegate() {
453 override fun onActivityCreated(
454 activity: Activity,
455 savedInstanceState: Bundle?
456 ) {
457 activity.window.onDecorViewReady {
458 val cleaner = ReferenceCleaner(
459 inputMethodManager,
460 mHField,
461 mServedViewField,
462 finishInputLockedMethod
463 )
464 val rootView = activity.window.decorView.rootView
465 val viewTreeObserver = rootView.viewTreeObserver
466 viewTreeObserver.addOnGlobalFocusChangeListener(cleaner)
467 }
468 }
469 })
470 }
471 },
472
473 /**
474 * When an activity is destroyed, the corresponding ViewRootImpl instance is released and ready to
475 * be garbage collected.
476 * Some time after that, ViewRootImpl#W receives a windowfocusChanged() callback, which it
477 * normally delegates to ViewRootImpl which in turn calls
478 * InputMethodManager#onPreWindowFocus which clears InputMethodManager#mCurRootView.
479 *
480 * Unfortunately, since the ViewRootImpl instance is garbage collectable it may be garbage
481 * collected before that happens.
482 * ViewRootImpl#W has a weak reference on ViewRootImpl, so that weak reference will then return
483 * null and the windowfocusChanged() callback will be ignored, leading to
484 * InputMethodManager#mCurRootView not being cleared.
485 *
486 * Filed here: https://issuetracker.google.com/u/0/issues/116078227
487 * Fixed here: https://android.googlesource.com/platform/frameworks/base/+/dff365ef4dc61239fac70953b631e92972a9f41f%5E%21/#F0
488 * InputMethodManager.mCurRootView is part of the unrestricted grey list on Android 9:
489 * https://android.googlesource.com/platform/frameworks/base/+/pie-release/config/hiddenapi-light-greylist.txt#6057
490 */
491 IMM_CUR_ROOT_VIEW {
492 override fun apply(application: Application) {
493 if (SDK_INT >= 29) {
494 return
495 }
496 val inputMethodManager = try {
497 application.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
498 } catch (ignored: Throwable) {
499 // https://github.com/square/leakcanary/issues/2140
500 SharkLog.d(ignored) { "Could not retrieve InputMethodManager service" }
501 return
502 }
503 val mCurRootViewField = try {
504 InputMethodManager::class.java.getDeclaredField("mCurRootView").apply {
505 isAccessible = true
506 }
507 } catch (ignored: Throwable) {
508 SharkLog.d(ignored) { "Could not read InputMethodManager.mCurRootView field" }
509 return
510 }
511 // Clear InputMethodManager.mCurRootView on activity destroy
512 application.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks
513 by noOpDelegate() {
514 override fun onActivityDestroyed(activity: Activity) {
515 try {
516 val rootView = mCurRootViewField[inputMethodManager] as View?
517 val isDestroyedActivity = rootView != null &&
518 activity.window != null &&
519 activity.window.decorView === rootView
520 val rootViewActivityContext = rootView?.context?.activityOrNull
521 val isChildWindowOfDestroyedActivity = rootViewActivityContext === activity
522 if (isDestroyedActivity || isChildWindowOfDestroyedActivity) {
523 mCurRootViewField[inputMethodManager] = null
524 }
525 } catch (ignored: Throwable) {
526 SharkLog.d(ignored) { "Could not update InputMethodManager.mCurRootView field" }
527 }
528 }
529 })
530 // Clear InputMethodManager.mCurRootView on window removal (e.g. dialog dismiss)
531 Curtains.onRootViewsChangedListeners += OnRootViewRemovedListener { removedRootView ->
532 val immRootView = mCurRootViewField[inputMethodManager] as View?
533 if (immRootView === removedRootView) {
534 mCurRootViewField[inputMethodManager] = null
535 }
536 }
537 }
538
539 private val Context.activityOrNull: Activity?
540 get() {
541 var context = this
542 while (true) {
543 if (context is Application) {
544 return null
545 }
546 if (context is Activity) {
547 return context
548 }
549 if (context is ContextWrapper) {
550 val baseContext = context.baseContext
551 // Prevent Stack Overflow.
552 if (baseContext === this) {
553 return null
554 }
555 context = baseContext
556 } else {
557 return null
558 }
559 }
560 }
561 },
562
563 /**
564 * Every editable TextView has an Editor instance which has a SpellChecker instance. SpellChecker
565 * is in charge of displaying the little squiggle spans that show typos. SpellChecker starts a
566 * SpellCheckerSession as needed and then closes it when the TextView is detached from the window.
567 * A SpellCheckerSession is in charge of communicating with the spell checker service (which lives
568 * in another process) through TextServicesManager.
569 *
570 * The SpellChecker sends the TextView content to the spell checker service every 400ms, ie every
571 * time the service calls back with a result the SpellChecker schedules another check for 400ms
572 * later.
573 *
574 * When the TextView is detached from the window, the spell checker closes the session. In practice,
575 * SpellCheckerSessionListenerImpl.mHandler is set to null and when the service calls
576 * SpellCheckerSessionListenerImpl.onGetSuggestions or
577 * SpellCheckerSessionListenerImpl.onGetSentenceSuggestions back from another process, there's a
578 * null check for SpellCheckerSessionListenerImpl.mHandler and the callback is dropped.
579 *
580 * Unfortunately, on Android M there's a race condition in how that's done. When the service calls
581 * back into our app process, the IPC call is received on a binder thread. That's when the null
582 * check happens. If the session is not closed at this point (mHandler not null), the callback is
583 * then posted to the main thread. If on the main thread the session is closed after that post but
584 * prior to that post being handled, then the post will still be processed, after the session has
585 * been closed.
586 *
587 * When the post is processed, SpellCheckerSession calls back into SpellChecker which in turns
588 * schedules a new spell check to be ran in 400ms. The check is an anonymous inner class
589 * (SpellChecker$1) stored as SpellChecker.mSpellRunnable and implementing Runnable. It is scheduled
590 * by calling [View.postDelayed]. As we've seen, at this point the session may be closed which means
591 * that the view has been detached. [View.postDelayed] behaves differently when a view is detached:
592 * instead of posting to the single [Handler] used by the view hierarchy, it enqueues the Runnable
593 * into ViewRootImpl.RunQueue, a static queue that holds on to "actions" to be executed. As soon as
594 * a view hierarchy is attached, the ViewRootImpl.RunQueue is processed and emptied.
595 *
596 * Unfortunately, that means that as long as no view hierarchy is attached, ie as long as there
597 * are no activities alive, the actions stay in ViewRootImpl.RunQueue. That means SpellChecker$1
598 * ends up being kept in memory. It holds on to SpellChecker which in turns holds on
599 * to the detached TextView and corresponding destroyed activity & view hierarchy.
600 *
601 * We have a fix for this! When the spell check session is closed, we replace
602 * SpellCheckerSession.mSpellCheckerSessionListener (which normally is the SpellChecker) with a
603 * no-op implementation. So even if callbacks are enqueued to the main thread handler, these
604 * callbacks will call the no-op implementation and SpellChecker will not be scheduling a spell
605 * check.
606 *
607 * Sources to corroborate:
608 *
609 * https://android.googlesource.com/platform/frameworks/base/+/marshmallow-release/core/java/android/view/textservice/SpellCheckerSession.java
610 * https://android.googlesource.com/platform/frameworks/base/+/marshmallow-release/core/java/android/view/textservice/TextServicesManager.java
611 * https://android.googlesource.com/platform/frameworks/base/+/marshmallow-release/core/java/android/widget/SpellChecker.java
612 * https://android.googlesource.com/platform/frameworks/base/+/marshmallow-release/core/java/android/view/ViewRootImpl.java
613 */
614 SPELL_CHECKER {
615 @TargetApi(23)
616 @SuppressLint("PrivateApi")
617 override fun apply(application: Application) {
618 if (SDK_INT != 23) {
619 return
620 }
621
622 try {
623 val textServiceClass = TextServicesManager::class.java
624 val getInstanceMethod = textServiceClass.getDeclaredMethod("getInstance")
625
626 val sServiceField = textServiceClass.getDeclaredField("sService")
627 sServiceField.isAccessible = true
628
629 val serviceStubInterface =
630 Class.forName("com.android.internal.textservice.ITextServicesManager")
631
632 val spellCheckSessionClass = Class.forName("android.view.textservice.SpellCheckerSession")
633 val mSpellCheckerSessionListenerField =
634 spellCheckSessionClass.getDeclaredField("mSpellCheckerSessionListener")
635 mSpellCheckerSessionListenerField.isAccessible = true
636
637 val spellCheckerSessionListenerImplClass =
638 Class.forName(
639 "android.view.textservice.SpellCheckerSession\$SpellCheckerSessionListenerImpl"
640 )
641 val listenerImplHandlerField =
642 spellCheckerSessionListenerImplClass.getDeclaredField("mHandler")
643 listenerImplHandlerField.isAccessible = true
644
645 val spellCheckSessionHandlerClass =
646 Class.forName("android.view.textservice.SpellCheckerSession\$1")
647 val outerInstanceField = spellCheckSessionHandlerClass.getDeclaredField("this$0")
648 outerInstanceField.isAccessible = true
649
650 val listenerInterface =
651 Class.forName("android.view.textservice.SpellCheckerSession\$SpellCheckerSessionListener")
652 val noOpListener = Proxy.newProxyInstance(
653 listenerInterface.classLoader, arrayOf(listenerInterface)
654 ) { _: Any, _: Method, _: kotlin.Array<Any>? ->
655 SharkLog.d { "Received call to no-op SpellCheckerSessionListener after session closed" }
656 }
657
658 // Ensure a TextServicesManager instance is created and TextServicesManager.sService set.
659 getInstanceMethod
660 .invoke(null)
661 val realService = sServiceField[null]!!
662
663 val spellCheckerListenerToSession = mutableMapOf<Any, Any>()
664
665 val proxyService = Proxy.newProxyInstance(
666 serviceStubInterface.classLoader, arrayOf(serviceStubInterface)
667 ) { _: Any, method: Method, args: kotlin.Array<Any>? ->
668 try {
669 if (method.name == "getSpellCheckerService") {
670 // getSpellCheckerService is called when the session is opened, which allows us to
671 // capture the corresponding SpellCheckerSession instance via
672 // SpellCheckerSessionListenerImpl.mHandler.this$0
673 val spellCheckerSessionListener = args!![3]
674 val handler = listenerImplHandlerField[spellCheckerSessionListener]!!
675 val spellCheckerSession = outerInstanceField[handler]!!
676 // We add to a map of SpellCheckerSessionListenerImpl to SpellCheckerSession
677 spellCheckerListenerToSession[spellCheckerSessionListener] = spellCheckerSession
678 } else if (method.name == "finishSpellCheckerService") {
679 // finishSpellCheckerService is called when the session is open. After the session has been
680 // closed, any pending work posted to SpellCheckerSession.mHandler should be ignored. We do
681 // so by replacing mSpellCheckerSessionListener with a no-op implementation.
682 val spellCheckerSessionListener = args!![0]
683 val spellCheckerSession =
684 spellCheckerListenerToSession.remove(spellCheckerSessionListener)!!
685 // We use the SpellCheckerSessionListenerImpl to find the corresponding SpellCheckerSession
686 // At this point in time the session was just closed to
687 // SpellCheckerSessionListenerImpl.mHandler is null, which is why we had to capture
688 // the SpellCheckerSession during the getSpellCheckerService call.
689 mSpellCheckerSessionListenerField[spellCheckerSession] = noOpListener
690 }
691 } catch (ignored: Exception) {
692 SharkLog.d(ignored) { "Unable to fix SpellChecker leak" }
693 }
694 // Standard delegation
695 try {
696 return@newProxyInstance if (args != null) {
697 method.invoke(realService, *args)
698 } else {
699 method.invoke(realService)
700 }
701 } catch (invocationException: InvocationTargetException) {
702 throw invocationException.targetException
703 }
704 }
705 sServiceField[null] = proxyService
706 } catch (ignored: Exception) {
707 SharkLog.d(ignored) { "Unable to fix SpellChecker leak" }
708 }
709 }
710 }
711
712 ;
713
714 protected abstract fun apply(application: Application)
715
716 private var applied = false
717
718 companion object {
719
720 private const val SAMSUNG = "samsung"
721 private const val LG = "LGE"
722
723 fun applyFixes(
724 application: Application,
725 fixes: Set<AndroidLeakFixes> = EnumSet.allOf(AndroidLeakFixes::class.java)
726 ) {
727 checkMainThread()
728 fixes.forEach { fix ->
729 if (!fix.applied) {
730 fix.apply(application)
731 fix.applied = true
732 } else {
733 SharkLog.d { "${fix.name} leak fix already applied." }
734 }
735 }
736 }
737
738 internal val backgroundHandler by lazy {
739 val handlerThread = HandlerThread("plumber-android-leaks")
740 handlerThread.start()
741 Handler(handlerThread.looper)
742 }
743
744 private fun Handler.onEachIdle(onIdle: () -> Unit) {
745 try {
746 // Unfortunately Looper.getQueue() is API 23. Looper.myQueue() is API 1.
747 // So we have to post to the handler thread to be able to obtain the queue for that
748 // thread from within that thread.
749 post {
750 Looper
751 .myQueue()
752 .addIdleHandler {
753 onIdle()
754 true
755 }
756 }
757 } catch (ignored: RuntimeException) {
758 // If the thread is quitting, posting to it will throw. There is no safe and atomic way
759 // to check if a thread is quitting first then post it it.
760 }
761 }
762
763 private fun findAllHandlerThreads(): List<HandlerThread> {
764 // Based on https://stackoverflow.com/a/1323480
765 var rootGroup = Thread.currentThread().threadGroup!!
766 while (rootGroup.parent != null) rootGroup = rootGroup.parent
767 var threads = arrayOfNulls<Thread>(rootGroup.activeCount())
768 while (rootGroup.enumerate(threads, true) == threads.size) {
769 threads = arrayOfNulls(threads.size * 2)
770 }
771 return threads.mapNotNull { if (it is HandlerThread) it else null }
772 }
773
774 internal fun Application.onActivityDestroyed(block: (Activity) -> Unit) {
775 registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks
776 by noOpDelegate() {
777 override fun onActivityDestroyed(activity: Activity) {
778 block(activity)
779 }
780 })
781 }
782
783 private fun Window.onDecorViewReady(callback: () -> Unit) {
784 if (peekDecorView() == null) {
785 onContentChanged {
786 callback()
787 return@onContentChanged false
788 }
789 } else {
790 callback()
791 }
792 }
793
794 private fun Window.onContentChanged(block: () -> Boolean) {
795 val callback = wrapCallback()
796 callback.onContentChangedCallbacks += block
797 }
798
799 private fun Window.wrapCallback(): WindowDelegateCallback {
800 val currentCallback = callback
801 return if (currentCallback is WindowDelegateCallback) {
802 currentCallback
803 } else {
804 val newCallback = WindowDelegateCallback(currentCallback)
805 callback = newCallback
806 newCallback
807 }
808 }
809
810 private class WindowDelegateCallback constructor(
811 private val delegate: Window.Callback
812 ) : FixedWindowCallback(delegate) {
813
814 val onContentChangedCallbacks = mutableListOf<() -> Boolean>()
815
816 override fun onContentChanged() {
817 onContentChangedCallbacks.removeAll { callback ->
818 !callback()
819 }
820 delegate.onContentChanged()
821 }
822 }
823 }
824 }
825