Search code examples
androidcordovacordova-pluginsandroid-looperandroid-threading

Android Runnable not executed by MainLooper


Brief description of application:

  • I have Cordova/Ionic application and Custom Cordova plugin for native code execution.
  • Plugin contains separate CameraActivity (extends FragmentActivity) to work with Camera (parts of code based on Camera2Basic example).
  • On launch Activity displays AnaliseFragment, where application captures every Camera frame and passes image to the analyser on backround thread.

Execution steps are:

  1. User presses button on Cordova UI
  2. Cordova executes native plugin method via cordova.exec(..)
  3. Native plugin starts CameraActivity for result via cordova.startActivityForResult(..)
  4. CameraActivity displays AnaliseFragment
  5. AnaliseFragment starts Camera capture session with two surfaces: first is displayed on TextureView and second analised by ImageAnaliser

Problem:

Rarely and randomly UI stops reacting on user and runnables not executed on UI thread. At the same time background threads continue working as normal: camera output is visible on TextureView and ImageAnaliser continue receive images from Camera.

Does anybody have any suggestion how to find/debug reason of such behavior? Or any ideas what can cause this?

I already tried:

  • log every lifecycle event of CameraActivity/AnaliseFragment = no calls between app normal state and ANR
  • add WAKELOCK to keep Cordova MainActivity alive = didn't help
  • log(trace) every method in AnalilseFragment and ImageAnaliser = nothing suspicious

Here is simplified code of AnaliseFragment:

public class AnaliseFragment extends Fragment {

private HandlerThread mBackgroundThread;
private Handler mBackgroundHandler;
private ImageAnalyser mImageAnalyser;

// listener is attached to camera capture session and receives every frame
private final ImageReader.OnImageAvailableListener mOnImageAvailableListener
    = new ImageReader.OnImageAvailableListener() {

    @Override
    public void onImageAvailable(ImageReader reader) {
        Image nextImage = reader.acquireLatestImage();
        mBackgroundHandler.post(() -> 
            try {
                mImageAnalyser.AnalizeNextImage(mImage);
            }
            finally {
                mImage.close();
            }
        );
    }
};

@Override
public void onViewCreated(final View view, Bundle savedInstanceState) {
    mImageAnalyser = new ImageAnalyser();
    mImageAnalyser.onResultAvailable(boolResult -> {
        // Runnable posted, but never executed
        new Handler(Looper.getMainLooper()).post(() -> reportToActivityAndUpdateUI(boolResult));
    });
}

@Override
public void onResume() {
    super.onResume();
    startBackgroundThread();
}

@Override
public void onPause() {
    stopBackgroundThread();
    super.onPause();
}

private void startBackgroundThread() {
    if (mBackgroundThread == null) {
        mBackgroundThread = new HandlerThread("MyBackground");
        mBackgroundThread.start();
        mBackgroundHandler = new Handler(mBackgroundThread.getLooper());
    }
}

private void stopBackgroundThread() {
    mBackgroundThread.quitSafely();
    try {
        mBackgroundThread.join();
        mBackgroundThread = null;
        mBackgroundHandler = null;
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
}

Simplified code for ImageAnalyser:

public class ImageAnalyser  {

public interface ResultAvailableListener {
    void onResult(bool boolResult);
}
private ResultAvailableListener mResultAvailableListener;   
public void onResultAvailable(ResultAvailableListener listener) { mResultAvailableListener = listener; }

public void AnalizeNextImage(Image image) {
    // Do heavy analysis and put result into theResult
    mResultAvailableListener.onResult(theResult);
}
}

Solution

  • After hours of profiling, debugging and code review I found, that

    issue was caused by incorrect View invalidation from background thread

    View.postInvalidate() method must be used - this method checks if View is still attached to window and then do invalidation. Instead I wrongly used View.invalidate(), when process my custom message from MainLooper, which rarely caused failures and made MainLooper stop processing any more messages.

    For those who maybe have same problem I added both correct and incorrect code.


    CORRECT:

    public class GraphicOverlayView extends View { ... }
    
    // Somewhere in background thread logic:
    
    private GraphicOverlayView mGraphicOverlayView;
    
    private void invalidateGraphicOverlayViewFromBackgroundThread(){
        mGraphicOverlayView.postInvalidate();
    };
    

    WRONG:

    public class GraphicOverlayView extends View { ... }
    
    // Somewhere in background thread logic:
    
    private GraphicOverlayView mGraphicOverlayView;
    
    private final int MSG_INVALIDATE_GRAPHICS_OVERLAY = 1;
    private Handler mUIHandler = new Handler(Looper.getMainLooper()){
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_INVALIDATE_GRAPHICS_OVERLAY:{
                    GraphicOverlayView overlay = (GraphicOverlayView)msg.obj;
                    // Next line can cause MainLooper stop processing other messages
                    overlay.invalidate();
                    break;
                }
                default:
                    super.handleMessage(msg);
            }
        }
    };
    private void invalidateGraphicOverlayViewFromBackgroundThread(){
        Message msg = new Message();
        msg.obj = mGraphicOverlayView;
        msg.what = MSG_INVALIDATE_GRAPHICS_OVERLAY;
        mUIHandler.dispatchMessage(msg);
    };