1 package leakcanary.internal 2 3 import android.app.Activity 4 import android.app.Application 5 import android.content.Context 6 import android.content.ContextWrapper 7 import android.os.Looper 8 import android.os.MessageQueue.IdleHandler 9 import android.view.View 10 import android.view.View.OnAttachStateChangeListener 11 import android.view.ViewTreeObserver.OnGlobalFocusChangeListener 12 import android.view.inputmethod.InputMethodManager 13 import java.lang.reflect.Field 14 import java.lang.reflect.Method 15 import shark.SharkLog 16 17 internal class ReferenceCleaner( 18 private val inputMethodManager: InputMethodManager, 19 private val mHField: Field, 20 private val mServedViewField: Field, 21 private val finishInputLockedMethod: Method 22 ) : IdleHandler, 23 OnAttachStateChangeListener, 24 OnGlobalFocusChangeListener { onGlobalFocusChangednull25 override fun onGlobalFocusChanged( 26 oldFocus: View?, 27 newFocus: View? 28 ) { 29 if (newFocus == null) { 30 return 31 } 32 oldFocus?.removeOnAttachStateChangeListener(this) 33 Looper.myQueue() 34 .removeIdleHandler(this) 35 newFocus.addOnAttachStateChangeListener(this) 36 } 37 onViewAttachedToWindownull38 override fun onViewAttachedToWindow(v: View) {} onViewDetachedFromWindownull39 override fun onViewDetachedFromWindow(v: View) { 40 v.removeOnAttachStateChangeListener(this) 41 Looper.myQueue() 42 .removeIdleHandler(this) 43 Looper.myQueue() 44 .addIdleHandler(this) 45 } 46 queueIdlenull47 override fun queueIdle(): Boolean { 48 clearInputMethodManagerLeak() 49 return false 50 } 51 clearInputMethodManagerLeaknull52 private fun clearInputMethodManagerLeak() { 53 try { 54 val lock = mHField[inputMethodManager] 55 if (lock == null) { 56 SharkLog.d { "InputMethodManager.mH was null, could not fix leak." } 57 return 58 } 59 // This is highly dependent on the InputMethodManager implementation. 60 synchronized(lock) { 61 val servedView = 62 mServedViewField[inputMethodManager] as View? 63 if (servedView != null) { 64 val servedViewAttached = 65 servedView.windowVisibility != View.GONE 66 if (servedViewAttached) { 67 // The view held by the IMM was replaced without a global focus change. Let's make 68 // sure we get notified when that view detaches. 69 // Avoid double registration. 70 servedView.removeOnAttachStateChangeListener(this) 71 servedView.addOnAttachStateChangeListener(this) 72 } else { // servedView is not attached. InputMethodManager is being stupid! 73 val activity = extractActivity(servedView.context) 74 if (activity == null || activity.window == null) { 75 // Unlikely case. Let's finish the input anyways. 76 finishInputLockedMethod.invoke(inputMethodManager) 77 } else { 78 val decorView = activity.window 79 .peekDecorView() 80 val windowAttached = 81 decorView.windowVisibility != View.GONE 82 // If the window is attached, we do nothing. The IMM is leaking a detached view 83 // hierarchy, but we haven't found a way to clear the reference without breaking 84 // the IMM behavior. 85 if (!windowAttached) { 86 finishInputLockedMethod.invoke(inputMethodManager) 87 } 88 } 89 } 90 } 91 } 92 } catch (ignored: Throwable) { 93 SharkLog.d(ignored) { "Could not fix leak" } 94 } 95 } 96 extractActivitynull97 private fun extractActivity(sourceContext: Context): Activity? { 98 var context = sourceContext 99 while (true) { 100 context = when (context) { 101 is Application -> { 102 return null 103 } 104 is Activity -> { 105 return context 106 } 107 is ContextWrapper -> { 108 val baseContext = 109 context.baseContext 110 // Prevent Stack Overflow. 111 if (baseContext === context) { 112 return null 113 } 114 baseContext 115 } 116 else -> { 117 return null 118 } 119 } 120 } 121 } 122 } 123