-
-
Save TreyCai/c78b61640db8650af4fe to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.app.Activity; | |
import android.app.Application; | |
import android.content.Context; | |
import android.content.ContextWrapper; | |
import android.os.Bundle; | |
import android.os.Looper; | |
import android.os.MessageQueue; | |
import android.util.Log; | |
import android.view.View; | |
import android.view.ViewTreeObserver; | |
import android.view.inputmethod.InputMethodManager; | |
import java.lang.reflect.Field; | |
import java.lang.reflect.InvocationTargetException; | |
import java.lang.reflect.Method; | |
import static android.content.Context.INPUT_METHOD_SERVICE; | |
import static android.os.Build.VERSION.SDK_INT; | |
import static android.os.Build.VERSION_CODES.KITKAT; | |
public class IMMLeaks { | |
static class ReferenceCleaner | |
implements MessageQueue.IdleHandler, View.OnAttachStateChangeListener, | |
ViewTreeObserver.OnGlobalFocusChangeListener { | |
private final InputMethodManager inputMethodManager; | |
private final Field mHField; | |
private final Field mServedViewField; | |
private final Method finishInputLockedMethod; | |
ReferenceCleaner(InputMethodManager inputMethodManager, Field mHField, Field mServedViewField, | |
Method finishInputLockedMethod) { | |
this.inputMethodManager = inputMethodManager; | |
this.mHField = mHField; | |
this.mServedViewField = mServedViewField; | |
this.finishInputLockedMethod = finishInputLockedMethod; | |
} | |
@Override public void onGlobalFocusChanged(View oldFocus, View newFocus) { | |
if (newFocus == null) { | |
return; | |
} | |
if (oldFocus != null) { | |
oldFocus.removeOnAttachStateChangeListener(this); | |
} | |
Looper.myQueue().removeIdleHandler(this); | |
newFocus.addOnAttachStateChangeListener(this); | |
} | |
@Override public void onViewAttachedToWindow(View v) { | |
} | |
@Override public void onViewDetachedFromWindow(View v) { | |
v.removeOnAttachStateChangeListener(this); | |
Looper.myQueue().removeIdleHandler(this); | |
Looper.myQueue().addIdleHandler(this); | |
} | |
@Override public boolean queueIdle() { | |
clearInputMethodManagerLeak(); | |
return false; | |
} | |
private void clearInputMethodManagerLeak() { | |
try { | |
Object lock = mHField.get(inputMethodManager); | |
// This is highly dependent on the InputMethodManager implementation. | |
synchronized (lock) { | |
View servedView = (View) mServedViewField.get(inputMethodManager); | |
if (servedView != null) { | |
boolean servedViewAttached = servedView.getWindowVisibility() != View.GONE; | |
if (servedViewAttached) { | |
// The view held by the IMM was replaced without a global focus change. Let's make | |
// sure we get notified when that view detaches. | |
// Avoid double registration. | |
servedView.removeOnAttachStateChangeListener(this); | |
servedView.addOnAttachStateChangeListener(this); | |
} else { | |
// servedView is not attached. InputMethodManager is being stupid! | |
Activity activity = extractActivity(servedView.getContext()); | |
if (activity == null || activity.getWindow() == null) { | |
// Unlikely case. Let's finish the input anyways. | |
finishInputLockedMethod.invoke(inputMethodManager); | |
} else { | |
View decorView = activity.getWindow().peekDecorView(); | |
boolean windowAttached = decorView.getWindowVisibility() != View.GONE; | |
if (!windowAttached) { | |
finishInputLockedMethod.invoke(inputMethodManager); | |
} else { | |
decorView.requestFocusFromTouch(); | |
} | |
} | |
} | |
} | |
} | |
} catch (IllegalAccessException | InvocationTargetException unexpected) { | |
Log.e("IMMLeaks", "Unexpected reflection exception", unexpected); | |
} | |
} | |
private Activity extractActivity(Context context) { | |
while (true) { | |
if (context instanceof Application) { | |
return null; | |
} else if (context instanceof Activity) { | |
return (Activity) context; | |
} else if (context instanceof ContextWrapper) { | |
Context baseContext = ((ContextWrapper) context).getBaseContext(); | |
// Prevent Stack Overflow. | |
if (baseContext == context) { | |
return null; | |
} | |
context = baseContext; | |
} else { | |
return null; | |
} | |
} | |
} | |
} | |
/** | |
* 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. | |
* | |
* Should be called from {@link Activity#onCreate(android.os.Bundle)} )}. | |
*/ | |
public static void fixFocusedViewLeak(Application application) { | |
// Don't know about other versions yet. | |
if (SDK_INT < KITKAT || SDK_INT > 22) { | |
return; | |
} | |
final InputMethodManager inputMethodManager = | |
(InputMethodManager) application.getSystemService(INPUT_METHOD_SERVICE); | |
final Field mServedViewField; | |
final Field mHField; | |
final Method finishInputLockedMethod; | |
final Method focusInMethod; | |
try { | |
mServedViewField = InputMethodManager.class.getDeclaredField("mServedView"); | |
mServedViewField.setAccessible(true); | |
mHField = InputMethodManager.class.getDeclaredField("mServedView"); | |
mHField.setAccessible(true); | |
finishInputLockedMethod = InputMethodManager.class.getDeclaredMethod("finishInputLocked"); | |
finishInputLockedMethod.setAccessible(true); | |
focusInMethod = InputMethodManager.class.getDeclaredMethod("focusIn", View.class); | |
focusInMethod.setAccessible(true); | |
} catch (NoSuchMethodException | NoSuchFieldException unexpected) { | |
Log.e("IMMLeaks", "Unexpected reflection exception", unexpected); | |
return; | |
} | |
application.registerActivityLifecycleCallbacks(new LifecycleCallbacksAdapter() { | |
@Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { | |
ReferenceCleaner cleaner = | |
new ReferenceCleaner(inputMethodManager, mHField, mServedViewField, | |
finishInputLockedMethod); | |
View decorView = activity.getWindow().peekDecorView(); | |
if (decorView != null) { | |
View rootView = activity.getWindow().getDecorView().getRootView(); | |
ViewTreeObserver viewTreeObserver = rootView.getViewTreeObserver(); | |
viewTreeObserver.addOnGlobalFocusChangeListener(cleaner); | |
} | |
} | |
}); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.app.Activity; | |
import android.app.Application; | |
import android.os.Bundle; | |
/** Helper to avoid implementing all lifecycle callback methods. */ | |
public class LifecycleCallbacksAdapter implements Application.ActivityLifecycleCallbacks { | |
@Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { | |
} | |
@Override public void onActivityStarted(Activity activity) { | |
} | |
@Override public void onActivityResumed(Activity activity) { | |
} | |
@Override public void onActivityPaused(Activity activity) { | |
} | |
@Override public void onActivityStopped(Activity activity) { | |
} | |
@Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) { | |
} | |
@Override public void onActivityDestroyed(Activity activity) { | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment