xref: /aosp_15_r20/external/leakcanary2/plumber-android-core/src/main/java/leakcanary/AndroidLeakFixes.kt (revision d9e8da70d8c9df9a41d7848ae506fb3115cae6e6)

<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