Saturday, February 1, 2020

Aleksandr Koscheev


LOOKING FOR A JOB


Right now I am in Cancún, Mexico.

Phones:

 (+521) 998 424 8890

Skype: ru.alxr

Email aleksandr.koscheev@gmail.com

LinkedIn profile: https://mx.linkedin.com/in/aleksandr-koscheev-b7260062

I am looking for job in Android development industry


Friday, August 3, 2018

Final solution for leaking context with InputMethodManager



Pretty annoying, right?
I've tried already solution with transparent DummyActivity but it has some negative effects.
Here is solution provided via leakcanary and code with minor modifications:

import android.annotation.SuppressLint;
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.view.View;
import android.view.ViewTreeObserver;
import android.view.inputmethod.InputMethodManager;

import com.twels.utils.Debug;

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;
import static android.os.Build.VERSION_CODES.N;


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() {
            Object lock;
            try {
                lock = mHField.get(inputMethodManager);
            } catch (IllegalAccessException e) {
                Debug
                        .e(e);
                return false;
            }
            Finalizer finalizer = new Finalizer(
                    lock,
                    inputMethodManager,
                    mServedViewField,
                    mHField,
                    finishInputLockedMethod,
                    this
            );
            finalizer.clearInputMethodManagerLeak();
            return false;
        }
    }

    /**
     * Fix for https://code.google.com/p/android/issues/detail?id=171190 .
     * <p>
     * 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.
     * <p>
     * Should be called from {@link Activity#onCreate(android.os.Bundle)} )}.
     */
    @SuppressLint("PrivateApi")
    @SuppressWarnings("JavaReflectionMemberAccess")
    public static void fixFocusedViewLeak(Application application) {       
        if (SDK_INT < KITKAT || SDK_INT >= N) {
            Debug
                    .get()
                    .with("IMMLeaks", "API OK")
                    .tag("FIX IMMLeaks")
                    .log();
            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 e) {
            Debug
                    .get()
                    .with("IMMLeaks", "PREPARATION FAIL")
                    .tag("FIX IMMLeaks")
                    .log();
            Debug
                    .e(e);
            return;
        } catch (NoSuchFieldException e) {
            Debug
                    .get()
                    .with("IMMLeaks", "PREPARATION FAIL")
                    .tag("FIX IMMLeaks")
                    .log();
            Debug
                    .e(e);
            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);
            }
        });
    }

    private static class Finalizer {

        Finalizer(Object lock,
                  InputMethodManager inputMethodManager,
                  Field mServedViewField,
                  Field mHField,
                  Method finishInputLockedMethod,
                  View.OnAttachStateChangeListener listener) {
            this.mLock = lock;
            this.mInputMethodManager = inputMethodManager;
            this.mFinishInputLockedMethod = finishInputLockedMethod;
            this.mHField = mHField;
            this.mServedViewField = mServedViewField;
            this.mListener = listener;
        }

        private final Object mLock;

        private final InputMethodManager mInputMethodManager;
        final Field mServedViewField;
        final Field mHField;
        final Method mFinishInputLockedMethod;
        final View.OnAttachStateChangeListener mListener;

        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;
                }
            }
        }

        private void clearInputMethodManagerLeak() {
            try {

                if (mLock == null) return;
                // This is highly dependent on the InputMethodManager implementation.
                synchronized (mLock) {
                    View servedView = (View) mServedViewField.get(mInputMethodManager);
                    if (servedView == null) return;
                    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(mListener);
                        servedView.addOnAttachStateChangeListener(mListener);
                    } 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.
                            mFinishInputLockedMethod.invoke(mInputMethodManager);
                            Debug
                                    .get()
                                    .with(this, "SUCCESS")
                                    .tag("FIX IMMLeaks")
                                    .log();
                        } else {
                            View decorView = activity.getWindow().peekDecorView();
                            boolean windowAttached = decorView.getWindowVisibility() != View.GONE;
                            if (!windowAttached) {
                                mFinishInputLockedMethod.invoke(mInputMethodManager);
                                Debug
                                        .get()
                                        .with(this, "SUCCESS")
                                        .tag("FIX IMMLeaks")
                                        .log();
                            } else {
                                decorView.requestFocusFromTouch();
                            }
                        }
                    }
                }
            } catch (IllegalAccessException e) {
                Debug
                        .get()
                        .with(this, "FAIL")
                        .tag("FIX IMMLeaks")
                        .log();
                Debug
                        .e(e);
            } catch (InvocationTargetException e) {
                Debug
                        .get()
                        .with(this, "FAIL")
                        .tag("FIX IMMLeaks")
                        .log();
                Debug
                        .e(e);
            }
        }

    }

}

and helper class 

import android.app.Activity;
import android.app.Application;
import android.os.Bundle;

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) {

    }

}

Friday, March 16, 2018

Tuesday, February 20, 2018

Realm and threads

Just created objects could be touched in any thread, framework does not mind.

new AsyncTask<Void, Void, Dummy>() {
    @Override    protected Dummy doInBackground(Void... voids) {
        Dummy dummy = new Dummy("a", "b");
        Realm realm = Realm.getDefaultInstance();
        realm.beginTransaction();
        realm.insertOrUpdate(dummy);
        realm.commitTransaction();

        Debug
                .get()
                .with(
                        this,
                        "THREAD %s %s - %s",
                        Thread.currentThread().getName(),
                        dummy.getKey(),
                        dummy.getValue()
                )
                .log();

        new Thread(new Runnable() {
            @Override            public void run() {
                Debug
                        .get()
                        .with(
                                this,
                                "THREAD %s %s - %s",
                                Thread.currentThread().getName(),
                                dummy.getKey(),
                                dummy.getValue()
                        )
                        .log();
                dummy.setValue("c");
                Realm realm = Realm.getDefaultInstance();
                realm.beginTransaction();
                realm.insertOrUpdate(dummy);
                realm.commitTransaction();
                Debug
                        .get()
                        .with(
                                this,
                                "UPDATED : THREAD %s %s - %s",
                                Thread.currentThread().getName(),
                                dummy.getKey(),
                                dummy.getValue()
                        )
                        .log();

            }
        }).start();
        return dummy;
    }

    @Override    protected void onPostExecute(Dummy dummy) {
        Debug
                .get()
                .with(
                        this,
                        "THREAD %s %s - %s",
                        Thread.currentThread().getName(),
                        dummy.getKey(),
                        dummy.getValue()
                )
                .log();
    }
}.execute();

Output is
THREAD AsyncTask #1 b - a
THREAD main b - a
THREAD Thread-6 b - a
UPDATED : THREAD Thread-6 b - c

Well, let's try to read the database.

new AsyncTask<Void, Void, Dummy>() {
    @Override    protected Dummy doInBackground(Void... voids) {
        Realm realm = Realm.getDefaultInstance();
        RealmQuery<Dummy> query = realm.where(Dummy.class);
        List<Dummy> list = query.findAll();
        Dummy result = null;
        for (Dummy dummy : list){
            result = dummy;
            Debug
                    .get()
                    .with(
                            this,
                            "THREAD %s %s - %s",
                            Thread.currentThread().getName(),
                            dummy.getKey(),
                            dummy.getValue()
                    )
                    .log();

            new Thread(new Runnable() {
                @Override                public void run() {
                    Debug
                            .get()
                            .with(
                                    this,
                                    "THREAD %s %s - %s",
                                    Thread.currentThread().getName(),
                                    dummy.getKey(),
                                    dummy.getValue()
                            )
                            .log();
                    dummy.setValue("c");
                    Realm realm = Realm.getDefaultInstance();
                    realm.beginTransaction();
                    realm.insertOrUpdate(dummy);
                    realm.commitTransaction();
                    Debug
                            .get()
                            .with(
                                    this,
                                    "UPDATED : THREAD %s %s - %s",
                                    Thread.currentThread().getName(),
                                    dummy.getKey(),
                                    dummy.getValue()
                            )
                            .log();

                }
            }).start();
            if (result != null) break;
        }
        return result;
    }

    @Override    protected void onPostExecute(Dummy dummy) {
        Debug
                .get()
                .with(
                        this,
                        "THREAD %s %s - %s",
                        Thread.currentThread().getName(),
                        dummy.getKey(),
                        dummy.getValue()
                )
                .log();
    }
}.execute();

Output is
THREAD AsyncTask #1 b - c
java.lang.IllegalStateException: Realm access from incorrect thread. 
Realm objects can only be accessed on the thread they were created.

OK, lets block onPostExecute.

All the same:
THREAD AsyncTask #1 b - c
java.lang.IllegalStateException: Realm access from incorrect thread.
Realm objects can only be accessed on the thread they were created.

OK, I can see, that you cannot touch RealmObject from different thread. 
Docs are right)))

What to do?
Realm offers AutoValue

Let's try.
Installation:
 Here is abstract DummyNotARealmObject class:
import com.google.auto.value.AutoValue;

@AutoValuepublic abstract class DummyNotARealmObject{

    public static DummyNotARealmObject create(String key, String value) {
        return new AutoValue_DummyNotARealmObject(key, value);
    }

    public abstract String getKey();
    public abstract String getValue();

}

... and modified asyncTask:

new AsyncTask<Void, Void, DummyNotARealmObject>() {
    @Override    protected DummyNotARealmObject doInBackground(Void... voids) {
        Realm realm = Realm.getDefaultInstance();
        RealmQuery<Dummy> query = realm.where(Dummy.class);
        List<Dummy> list = query.findAll();
        final DummyNotARealmObject result;
        for (Dummy dummy : list){
            result = DummyNotARealmObject.create(
                    dummy.getKey(),
                    dummy.getValue()
            );
            Debug
                    .get()
                    .with(
                            this,
                            "THREAD %s %s - %s",
                            Thread.currentThread().getName(),
                            dummy.getKey(),
                            dummy.getValue()
                    )
                    .log();

            new Thread(new Runnable() {
                @Override                public void run() {
                    Debug
                            .get()
                            .with(
                                    this,
                                    "THREAD %s %s - %s",
                                    Thread.currentThread().getName(),
                                    result.getKey(),
                                    result.getValue()
                            )
                            .log();
                }
            }).start();
            return result;
        }
        return null;
    }

    @Override    protected void onPostExecute(DummyNotARealmObject dummy) {
        Debug
                .get()
                .with(
                        this,
                        "THREAD %s %s - %s",
                        Thread.currentThread().getName(),
                        dummy.getKey(),
                        dummy.getValue()
                )
                .log();
    }
}.execute();

Output:
THREAD AsyncTask #1 b - c
THREAD main b - c
THREAD Thread-6 b - c
As you can see, now we can easily get access to our Dummy-object's fields from another thread.
All we need is to create abstract class and declare constructor and getters.
This is not as simple as when we use SQLite but I guess Realm is worth it.