1

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);
}
}
Andris
  • 1,262
  • 1
  • 15
  • 24
  • Do you release the image resources? I don't see any calls to `https://developer.android.com/reference/android/media/Image.html#close()` – Andrei Mărcuţ Apr 12 '19 at 08:19
  • Yes, I call close() method in finally blocks, where needed. If not called, I would not be able to get more images, than buffer of Camera capture session. I updated code example, to see how close() method is called – Andris Apr 12 '19 at 08:31
  • Maybe your result never becomes available.. – Pierre Apr 12 '19 at 08:45
  • I added logging in fist lines of onResultAvailable and reportToActivityAndUpdateUI and see in Logcat, that onResultAvailable is called, but reportToActivityAndUpdateUI doesn't. – Andris Apr 12 '19 at 09:10
  • you only dispatch the result once in onViewCreated via `mImageAnalyser.onResultAvailable` . You should dispatch the result right after processing each image. – Andrei Mărcuţ Apr 12 '19 at 09:17
  • To be more clear I added part of ImageAnalyser code - onResultAvailable is called whenever any result available after image analyzing. – Andris Apr 12 '19 at 09:32
  • AnalyzeNextImage is executed in the background thread. You have to dispatch the result from the UI thread. – Andrei Mărcuţ Apr 12 '19 at 14:30
  • Yes, onResultAvailable is executed on background thread, therefore I post reportToActivityAndUpdateUI to MainLooper, which is Looper of UI thread. – Andris Apr 13 '19 at 17:45

2 Answers2

1

There is some long-running operation in UI-thread. Try profile your app to figure out what does block your main thread.

Andrew Churilo
  • 2,229
  • 1
  • 17
  • 26
0

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);
};
Andris
  • 1,262
  • 1
  • 15
  • 24