package io.blackbox_vision.mvphelpers.utils.bugfix; import android.annotation.TargetApi; 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 io.blackbox_vision.mvphelpers.utils.bugfix.helper.LifecycleCallbacksAdapter; import static android.content.Context.INPUT_METHOD_SERVICE; import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1; @TargetApi(ICE_CREAM_SANDWICH_MR1) @SuppressWarnings("all") public final class IMMLeaks { private static final String TAG = IMMLeaks.class.getSimpleName(); private static final String CUR_ROOT_VIEW = "mCurRootView"; private static final String NEXT_SERVED_VIEW = "mNextServedView"; private static final String SERVED_VIEW = "mServedView"; private static final String[] LEAKED_VIEWS = new String[] { CUR_ROOT_VIEW, NEXT_SERVED_VIEW, SERVED_VIEW }; 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 unexpected) { Log.e(TAG, "Unexpected reflection exception", unexpected); } catch (InvocationTargetException unexpected) { Log.e(TAG, "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(Bundle)} )}. */ private static void fixFocusedViewLeak(Application application, String fieldName) { // Don't know about other versions yet. if (SDK_INT < ICE_CREAM_SANDWICH_MR1 || SDK_INT > 24) { 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(fieldName); mServedViewField.setAccessible(true); mHField = InputMethodManager.class.getDeclaredField(fieldName); mHField.setAccessible(true); finishInputLockedMethod = InputMethodManager.class.getDeclaredMethod("finishInputLocked"); finishInputLockedMethod.setAccessible(true); focusInMethod = InputMethodManager.class.getDeclaredMethod("focusIn", View.class); focusInMethod.setAccessible(true); } catch (NoSuchMethodException unexpected) { Log.e(TAG, "Unexpected reflection exception", unexpected); return; } catch (NoSuchFieldException unexpected) { Log.e(TAG, "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 rootView = activity.getWindow().getDecorView().getRootView(); ViewTreeObserver viewTreeObserver = rootView.getViewTreeObserver(); viewTreeObserver.addOnGlobalFocusChangeListener(cleaner); } }); } /** * After digging a little bit over this bug, I have discovered that more views can produce this it * * @param application */ public static void fixAllPossiblyFocusedViewLeaks(Application application) { for (int i = 0; i < LEAKED_VIEWS.length; i++) { fixFocusedViewLeak(application, LEAKED_VIEWS[i]); } } }