/*
  ==============================================================================

   This file is part of the JUCE library.
   Copyright (c) 2022 - Raw Material Software Limited

   JUCE is an open source library subject to commercial or open-source
   licensing.

   By using JUCE, you agree to the terms of both the JUCE 7 End-User License
   Agreement and JUCE Privacy Policy.

   End User License Agreement: www.juce.com/juce-7-licence
   Privacy Policy: www.juce.com/juce-privacy-policy

   Or: You may also use this code under the terms of the GPL v3 (see
   www.gnu.org/licenses).

   JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
   EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
   DISCLAIMED.

  ==============================================================================
*/

//==============================================================================
// This byte-code is generated from:
//
// native/java/com/rmsl/juce/CameraCaptureSessionCaptureCallback.java
// native/java/com/rmsl/juce/CameraCaptureSessionStateCallback.java
// native/java/com/rmsl/juce/CameraDeviceStateCallback.java
// native/java/com/rmsl/juce/JuceOrientationEventListener.java
//
// files with min sdk version 21
// See juce_core/native/java/README.txt on how to generate this byte-code.
static const uint8 CameraSupportByteCode[] =
{31,139,8,8,187,110,161,94,0,3,67,97,109,101,114,97,83,117,112,112,111,114,116,66,121,116,101,67,111,100,101,46,100,101,120,0,
149,152,93,108,28,213,21,199,207,157,157,221,89,239,174,215,227,181,243,217,36,56,95,182,3,113,54,137,227,40,101,29,59,198,113,
192,102,133,169,215,182,34,3,45,147,221,73,188,176,222,89,102,215,139,171,86,37,68,84,208,151,42,80,90,168,4,136,170,20,181,15,
84,110,9,132,7,30,82,21,149,240,0,10,136,190,165,18,84,133,7,154,7,132,42,20,85,60,244,127,63,198,158,93,175,93,99,233,183,
255,51,115,206,61,247,222,115,239,140,103,38,103,47,68,14,246,246,209,213,103,22,174,255,247,133,96,249,25,246,233,195,151,255,
18,189,241,193,31,255,57,248,226,196,252,238,238,4,81,137,136,22,166,143,192,146,127,79,181,18,157,38,121,190,13,92,99,68,91,
160,219,52,162,32,244,174,0,81,55,247,67,117,232,34,126,110,198,136,14,194,249,86,152,232,10,248,2,36,154,136,14,131,62,112,12,
220,7,170,224,50,248,26,116,69,136,190,15,158,4,127,6,255,0,70,148,232,40,248,1,120,22,188,11,190,2,29,200,191,27,116,129,219,
120,95,160,15,164,192,9,112,39,24,7,167,193,131,32,7,242,192,1,85,240,99,240,24,120,18,252,22,188,7,110,128,182,102,162,65,48,7,
158,6,175,129,143,192,87,160,57,78,212,9,70,192,3,160,10,30,3,191,6,47,131,63,128,55,193,219,224,93,240,33,248,12,124,13,162,
45,68,123,193,16,200,128,7,64,9,252,4,252,28,60,7,126,3,46,131,43,224,125,112,29,124,6,254,13,254,3,190,1,97,147,168,21,236,2,
221,166,172,55,95,3,3,160,196,132,50,18,202,70,40,17,97,58,132,97,19,186,39,52,37,132,19,150,146,18,106,29,219,193,6,176,17,
116,42,109,85,107,190,73,217,139,72,188,89,217,111,25,114,221,185,125,5,246,119,148,125,21,246,86,101,127,236,179,175,251,98,
254,5,123,155,178,191,132,189,93,217,55,97,239,80,182,142,9,220,162,236,118,216,29,202,222,227,179,143,248,236,147,176,119,41,
123,18,246,78,101,223,239,59,159,131,189,91,217,5,216,123,148,189,0,123,175,178,159,240,217,23,125,246,11,190,156,191,247,229,
92,12,243,186,50,234,23,245,77,208,184,168,177,60,110,81,106,42,13,168,122,234,74,67,74,99,98,69,120,251,176,210,56,237,19,26,
163,91,133,54,211,109,66,155,104,191,208,8,245,8,141,210,1,161,27,105,80,104,59,157,16,186,129,134,132,182,210,29,106,92,195,66,
219,232,164,24,159,38,250,137,99,183,116,43,77,42,61,168,244,144,210,195,66,77,26,80,58,162,244,148,210,59,149,166,197,124,
101,222,22,204,172,87,233,17,161,6,245,169,227,163,66,55,137,60,92,239,18,186,153,70,213,241,152,168,151,172,144,137,138,29,83,
245,187,155,228,254,101,190,58,50,165,31,155,164,230,35,207,121,245,213,148,38,212,70,110,81,126,175,238,94,158,83,202,111,
42,127,172,110,125,92,229,15,34,51,247,247,154,242,250,42,153,60,126,6,151,208,253,237,12,189,199,224,231,215,222,113,83,230,
200,92,32,154,122,130,145,241,184,241,75,227,85,227,114,213,8,146,63,238,212,170,113,161,154,184,241,85,227,12,17,23,193,76,248,
88,79,155,242,90,207,184,136,171,32,238,17,227,167,236,87,213,80,88,68,201,245,228,227,126,208,203,247,52,226,126,129,184,139,
198,43,236,13,253,111,213,112,147,136,74,160,87,126,223,200,155,178,166,153,63,33,238,117,196,45,26,239,24,127,215,63,15,226,
198,83,141,68,68,172,142,190,121,77,202,166,172,87,201,100,162,38,154,240,105,98,45,126,164,124,153,14,141,74,39,18,164,29,90,
246,93,168,241,181,213,248,126,86,227,107,175,241,93,172,241,109,168,241,61,87,227,219,168,124,114,156,47,45,141,83,19,227,12,
248,198,249,234,82,187,0,218,109,175,201,249,90,141,111,135,240,5,145,147,223,111,47,249,125,147,183,212,180,123,187,166,93,
135,240,133,68,75,162,191,122,99,57,24,160,161,224,76,66,247,141,229,253,165,118,58,218,117,138,118,222,126,103,10,205,119,61,
48,181,219,229,185,160,82,67,249,12,159,47,33,242,115,13,146,252,127,225,217,222,181,33,143,155,136,137,76,203,199,225,154,248,
38,177,71,252,199,81,213,135,55,174,160,178,131,106,140,94,31,242,255,148,204,31,80,118,120,41,70,230,245,236,168,106,99,168,
92,92,67,253,249,98,190,50,64,221,195,214,156,237,90,195,86,169,50,239,218,25,187,92,206,59,69,117,52,108,21,10,103,172,236,195,
7,30,178,170,22,117,54,138,204,84,172,74,93,220,14,25,119,210,174,230,179,118,3,63,27,37,54,70,59,199,230,179,246,184,155,
183,139,136,64,162,145,42,172,116,190,92,177,139,182,43,3,183,164,173,98,206,117,242,185,100,214,41,226,124,37,57,204,117,161,
146,162,193,37,215,172,229,230,30,181,92,59,153,21,189,30,78,54,26,229,158,186,9,165,232,248,183,76,80,51,143,20,37,191,93,243,
20,245,253,191,6,178,92,245,253,116,173,175,89,138,246,173,21,40,134,114,202,202,23,32,235,9,157,176,31,153,183,203,40,115,247,
58,66,203,243,5,68,246,172,30,57,233,84,172,66,93,248,242,188,170,121,251,209,228,106,251,32,69,189,233,172,51,151,116,231,
202,133,228,67,216,48,13,171,187,98,113,15,173,163,81,93,161,247,53,108,210,96,15,167,104,127,93,232,90,27,57,69,108,154,180,
233,81,48,70,129,233,177,81,254,147,38,29,63,194,156,161,32,126,96,135,132,140,145,193,53,61,54,38,79,164,211,104,152,70,32,34,
244,105,238,15,78,11,47,14,224,99,51,20,146,85,166,173,217,6,115,28,202,86,242,85,155,246,52,242,121,69,115,230,74,5,187,98,231,
104,231,26,81,124,239,32,100,239,26,33,247,186,206,57,23,103,16,118,235,26,97,25,190,181,138,89,123,232,140,227,242,94,247,
175,35,118,121,140,187,214,138,174,88,34,99,195,74,12,23,28,62,178,221,13,125,78,241,108,254,220,242,44,119,172,25,148,163,45,
141,252,19,182,149,251,33,109,202,174,216,54,178,227,237,43,28,39,243,101,220,213,138,118,150,143,121,227,10,247,136,235,58,
110,131,124,227,37,108,171,28,25,89,121,39,164,205,57,225,243,237,191,225,89,171,120,14,33,65,91,164,48,206,202,235,158,162,103,
93,36,187,103,126,238,140,237,146,62,235,148,43,212,196,127,39,157,169,178,77,225,165,221,146,104,176,55,90,234,119,66,91,
163,117,223,188,234,42,111,93,99,77,205,21,43,24,94,90,175,214,149,171,19,171,89,139,184,83,172,41,164,129,235,79,76,27,57,84,
169,218,97,173,172,142,225,45,89,212,89,118,82,115,9,35,200,91,5,121,155,34,163,228,218,252,254,68,186,139,210,147,225,202,251,
34,133,92,233,143,148,213,108,70,145,176,44,55,2,181,148,103,157,249,66,238,14,84,70,53,174,204,230,203,212,84,201,207,161,173,
53,87,162,30,109,96,218,136,247,247,244,208,209,192,212,68,198,136,95,162,126,24,41,110,12,195,24,231,198,168,54,53,105,196,
127,71,227,129,169,201,126,126,98,90,159,154,56,13,235,35,218,207,6,120,115,234,101,83,70,124,134,190,43,101,80,202,136,148,187,
165,116,169,208,3,172,155,31,30,145,114,187,214,125,59,215,19,242,176,51,48,112,236,123,70,252,56,15,187,135,159,32,13,15,
207,26,211,244,243,231,245,197,8,187,128,167,188,6,68,217,181,8,99,55,193,203,81,198,174,130,47,193,243,49,98,161,160,166,181,
162,237,59,177,70,237,54,177,79,98,140,125,3,158,111,102,236,18,184,134,151,191,160,30,208,182,161,205,141,230,250,248,157,236,
169,56,99,175,128,43,224,58,30,225,52,166,107,123,31,63,175,223,140,115,127,23,123,182,69,190,215,120,207,117,158,122,223,88,
248,51,143,247,157,133,63,19,121,223,90,188,119,125,254,189,133,171,247,205,37,68,203,223,93,88,135,124,175,231,223,94,152,41,
223,221,249,59,188,214,33,243,243,239,49,1,21,195,223,105,248,203,52,111,43,222,167,76,57,14,254,189,231,127,249,28,121,55,40,18,
0,0,0,0};

#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK)    \
 STATICMETHOD (valueOf, "valueOf", "(Ljava/lang/String;)Landroid/graphics/Bitmap$CompressFormat;")

DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidBitmapCompressFormat, "android/graphics/Bitmap$CompressFormat", 21)
#undef JNI_CLASS_MEMBERS

#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
 METHOD (close,                "close",                "()V") \
 METHOD (createCaptureRequest, "createCaptureRequest", "(I)Landroid/hardware/camera2/CaptureRequest$Builder;") \
 METHOD (createCaptureSession, "createCaptureSession", "(Ljava/util/List;Landroid/hardware/camera2/CameraCaptureSession$StateCallback;Landroid/os/Handler;)V")

DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidCameraDevice, "android/hardware/camera2/CameraDevice", 21)
#undef JNI_CLASS_MEMBERS

#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
 METHOD (close,     "close",     "()V") \
 METHOD (getPlanes, "getPlanes", "()[Landroid/media/Image$Plane;")

DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidImage, "android/media/Image", 21)
#undef JNI_CLASS_MEMBERS

#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
 METHOD (getBuffer, "getBuffer", "()Ljava/nio/ByteBuffer;")

DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidImagePlane, "android/media/Image$Plane", 21)
#undef JNI_CLASS_MEMBERS

#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
 METHOD (acquireLatestImage,          "acquireLatestImage",          "()Landroid/media/Image;") \
 METHOD (close,                       "close",                       "()V") \
 METHOD (getSurface,                  "getSurface",                  "()Landroid/view/Surface;") \
 METHOD (setOnImageAvailableListener, "setOnImageAvailableListener", "(Landroid/media/ImageReader$OnImageAvailableListener;Landroid/os/Handler;)V") \
 STATICMETHOD (newInstance, "newInstance", "(IIII)Landroid/media/ImageReader;")

DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidImageReader, "android/media/ImageReader", 21)
#undef JNI_CLASS_MEMBERS

#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
 METHOD (constructor,             "<init>",                  "()V") \
 METHOD (getSurface,              "getSurface",              "()Landroid/view/Surface;") \
 METHOD (prepare,                 "prepare",                 "()V") \
 METHOD (release,                 "release",                 "()V") \
 METHOD (setAudioEncoder,         "setAudioEncoder",         "(I)V") \
 METHOD (setAudioSource,          "setAudioSource",          "(I)V") \
 METHOD (setOnErrorListener,      "setOnErrorListener",      "(Landroid/media/MediaRecorder$OnErrorListener;)V") \
 METHOD (setOnInfoListener,       "setOnInfoListener",       "(Landroid/media/MediaRecorder$OnInfoListener;)V") \
 METHOD (setOrientationHint,      "setOrientationHint",      "(I)V") \
 METHOD (setOutputFile,           "setOutputFile",           "(Ljava/lang/String;)V") \
 METHOD (setOutputFormat,         "setOutputFormat",         "(I)V") \
 METHOD (setVideoEncoder,         "setVideoEncoder",         "(I)V") \
 METHOD (setVideoEncodingBitRate, "setVideoEncodingBitRate", "(I)V") \
 METHOD (setVideoFrameRate,       "setVideoFrameRate",       "(I)V") \
 METHOD (setVideoSize,            "setVideoSize",            "(II)V") \
 METHOD (setVideoSource,          "setVideoSource",          "(I)V") \
 METHOD (start,                   "start",                   "()V") \
 METHOD (stop,                    "stop",                    "()V")

DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidMediaRecorder, "android/media/MediaRecorder", 21)
#undef JNI_CLASS_MEMBERS

#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
 METHOD (constructor,               "<init>",                    "(Landroid/content/Context;)V") \
 METHOD (getSurfaceTexture,         "getSurfaceTexture",         "()Landroid/graphics/SurfaceTexture;") \
 METHOD (isAvailable,               "isAvailable",               "()Z") \
 METHOD (setSurfaceTextureListener, "setSurfaceTextureListener", "(Landroid/view/TextureView$SurfaceTextureListener;)V") \
 METHOD (setTransform,              "setTransform",              "(Landroid/graphics/Matrix;)V")

DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidTextureView, "android/view/TextureView", 21)
#undef JNI_CLASS_MEMBERS

#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
 METHOD (constructor, "<init>", "(Landroid/graphics/SurfaceTexture;)V")

DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidSurface, "android/view/Surface", 21)
#undef JNI_CLASS_MEMBERS

#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
 METHOD (setDefaultBufferSize, "setDefaultBufferSize", "(II)V")

DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidSurfaceTexture, "android/graphics/SurfaceTexture", 21)
#undef JNI_CLASS_MEMBERS

#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
 METHOD (getOutputSizesForClass,      "getOutputSizes",       "(Ljava/lang/Class;)[Landroid/util/Size;") \
 METHOD (getOutputSizesForFormat,     "getOutputSizes",       "(I)[Landroid/util/Size;") \
 METHOD (isOutputSupportedFor,        "isOutputSupportedFor", "(I)Z") \
 METHOD (isOutputSupportedForSurface, "isOutputSupportedFor", "(Landroid/view/Surface;)Z")

DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidStreamConfigurationMap, "android/hardware/camera2/params/StreamConfigurationMap", 21)
#undef JNI_CLASS_MEMBERS

#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
 METHOD (constructor, "<init>",      "()V") \
 METHOD (toByteArray, "toByteArray", "()[B") \
 METHOD (size,        "size",        "()I")

DECLARE_JNI_CLASS_WITH_MIN_SDK (ByteArrayOutputStream, "java/io/ByteArrayOutputStream", 21)
#undef JNI_CLASS_MEMBERS

#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
 METHOD (abortCaptures,       "abortCaptures",       "()V") \
 METHOD (capture,             "capture",             "(Landroid/hardware/camera2/CaptureRequest;Landroid/hardware/camera2/CameraCaptureSession$CaptureCallback;Landroid/os/Handler;)I") \
 METHOD (close,               "close",               "()V") \
 METHOD (setRepeatingRequest, "setRepeatingRequest", "(Landroid/hardware/camera2/CaptureRequest;Landroid/hardware/camera2/CameraCaptureSession$CaptureCallback;Landroid/os/Handler;)I") \
 METHOD (stopRepeating,       "stopRepeating",       "()V")

DECLARE_JNI_CLASS_WITH_MIN_SDK (CameraCaptureSession, "android/hardware/camera2/CameraCaptureSession", 21)
#undef JNI_CLASS_MEMBERS

#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
 METHOD (get,     "get",     "(Landroid/hardware/camera2/CameraCharacteristics$Key;)Ljava/lang/Object;") \
 METHOD (getKeys, "getKeys", "()Ljava/util/List;") \
 STATICFIELD (CONTROL_AF_AVAILABLE_MODES,      "CONTROL_AF_AVAILABLE_MODES",      "Landroid/hardware/camera2/CameraCharacteristics$Key;") \
 STATICFIELD (LENS_FACING,                     "LENS_FACING",                     "Landroid/hardware/camera2/CameraCharacteristics$Key;") \
 STATICFIELD (SCALER_STREAM_CONFIGURATION_MAP, "SCALER_STREAM_CONFIGURATION_MAP", "Landroid/hardware/camera2/CameraCharacteristics$Key;") \
 STATICFIELD (SENSOR_ORIENTATION,              "SENSOR_ORIENTATION",              "Landroid/hardware/camera2/CameraCharacteristics$Key;")

DECLARE_JNI_CLASS_WITH_MIN_SDK (CameraCharacteristics, "android/hardware/camera2/CameraCharacteristics", 21)
#undef JNI_CLASS_MEMBERS

#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
 METHOD (getName, "getName", "()Ljava/lang/String;")

DECLARE_JNI_CLASS_WITH_MIN_SDK (CameraCharacteristicsKey, "android/hardware/camera2/CameraCharacteristics$Key", 21)
#undef JNI_CLASS_MEMBERS

#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
 METHOD (getCameraCharacteristics, "getCameraCharacteristics", "(Ljava/lang/String;)Landroid/hardware/camera2/CameraCharacteristics;") \
 METHOD (getCameraIdList,          "getCameraIdList",          "()[Ljava/lang/String;") \
 METHOD (openCamera,               "openCamera",               "(Ljava/lang/String;Landroid/hardware/camera2/CameraDevice$StateCallback;Landroid/os/Handler;)V")

DECLARE_JNI_CLASS_WITH_MIN_SDK (CameraManager, "android/hardware/camera2/CameraManager", 21)
#undef JNI_CLASS_MEMBERS

#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
 STATICFIELD (CONTROL_AE_PRECAPTURE_TRIGGER, "CONTROL_AE_PRECAPTURE_TRIGGER", "Landroid/hardware/camera2/CaptureRequest$Key;") \
 STATICFIELD (CONTROL_AF_MODE,               "CONTROL_AF_MODE",               "Landroid/hardware/camera2/CaptureRequest$Key;") \
 STATICFIELD (CONTROL_AF_TRIGGER,            "CONTROL_AF_TRIGGER",            "Landroid/hardware/camera2/CaptureRequest$Key;") \
 STATICFIELD (CONTROL_MODE,                  "CONTROL_MODE",                  "Landroid/hardware/camera2/CaptureRequest$Key;")

DECLARE_JNI_CLASS_WITH_MIN_SDK (CaptureRequest, "android/hardware/camera2/CaptureRequest", 21)
#undef JNI_CLASS_MEMBERS

#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
 METHOD (addTarget, "addTarget", "(Landroid/view/Surface;)V") \
 METHOD (build,     "build",     "()Landroid/hardware/camera2/CaptureRequest;") \
 METHOD (set,       "set",       "(Landroid/hardware/camera2/CaptureRequest$Key;Ljava/lang/Object;)V")

DECLARE_JNI_CLASS_WITH_MIN_SDK (CaptureRequestBuilder, "android/hardware/camera2/CaptureRequest$Builder", 21)
#undef JNI_CLASS_MEMBERS

#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
 METHOD (get, "get", "(Landroid/hardware/camera2/CaptureResult$Key;)Ljava/lang/Object;") \
 STATICFIELD (CONTROL_AE_STATE, "CONTROL_AE_STATE", "Landroid/hardware/camera2/CaptureResult$Key;") \
 STATICFIELD (CONTROL_AF_STATE, "CONTROL_AF_STATE", "Landroid/hardware/camera2/CaptureResult$Key;")

DECLARE_JNI_CLASS_WITH_MIN_SDK (CaptureResult, "android/hardware/camera2/CaptureResult", 21)
#undef JNI_CLASS_MEMBERS

//==============================================================================
class AndroidRunnable  : public juce::AndroidInterfaceImplementer
{
public:
    struct Owner
    {
        virtual ~Owner() {}

        virtual void run() = 0;
    };

    AndroidRunnable (Owner& ownerToUse)
        : owner (ownerToUse)
    {}

private:
    Owner& owner;

    jobject invoke (jobject proxy, jobject method, jobjectArray args) override
    {
        auto* env = getEnv();
        auto methodName = juce::juceString ((jstring) env->CallObjectMethod (method, JavaMethod.getName));

        if (methodName == "run")
        {
            owner.run();
            return nullptr;
        }

        // invoke base class
        return AndroidInterfaceImplementer::invoke (proxy, method, args);
    }
};

//==============================================================================
class TextureViewSurfaceTextureListener     : public AndroidInterfaceImplementer
{
public:
    struct Owner
    {
        virtual ~Owner() {}

        virtual void onSurfaceTextureAvailable (LocalRef<jobject>& surface, int width, int height) = 0;
        virtual bool onSurfaceTextureDestroyed (LocalRef<jobject>& surface) = 0;
        virtual void onSurfaceTextureSizeChanged (LocalRef<jobject>& surface, int width, int height) = 0;
        virtual void onSurfaceTextureUpdated (LocalRef<jobject>& surface) = 0;
    };

    TextureViewSurfaceTextureListener (Owner& ownerToUse)
        : owner (ownerToUse)
    {}

    jobject invoke (jobject proxy, jobject method, jobjectArray args) override
    {
        auto* env = getEnv();

        auto methodName = juceString ((jstring) env->CallObjectMethod (method, JavaMethod.getName));

        int numArgs = args != nullptr ? env->GetArrayLength (args) : 0;

        if (methodName == "onSurfaceTextureAvailable" && numArgs == 3)
        {
            auto surface = LocalRef<jobject> (env->GetObjectArrayElement (args, 0));
            auto width   = LocalRef<jobject> (env->GetObjectArrayElement (args, 1));
            auto height  = LocalRef<jobject> (env->GetObjectArrayElement (args, 2));

            auto widthInt  = env->CallIntMethod (width, JavaInteger.intValue);
            auto heightInt = env->CallIntMethod (height, JavaInteger.intValue);

            owner.onSurfaceTextureAvailable (surface, widthInt, heightInt);
            return nullptr;
        }
        else if (methodName == "onSurfaceTextureDestroyed" && numArgs == 1)
        {
            auto surface = LocalRef<jobject> (env->GetObjectArrayElement (args, 0));
            auto result = owner.onSurfaceTextureDestroyed (surface);

            return env->CallStaticObjectMethod (JavaBoolean, JavaBoolean.valueOf, result);
        }
        else if (methodName == "onSurfaceTextureSizeChanged" && numArgs == 3)
        {
            auto surface = LocalRef<jobject> (env->GetObjectArrayElement (args, 0));
            auto width   = LocalRef<jobject> (env->GetObjectArrayElement (args, 1));
            auto height  = LocalRef<jobject> (env->GetObjectArrayElement (args, 2));

            auto widthInt  = env->CallIntMethod (width, JavaInteger.intValue);
            auto heightInt = env->CallIntMethod (height, JavaInteger.intValue);

            owner.onSurfaceTextureSizeChanged (surface, widthInt, heightInt);
            return nullptr;
        }
        else if (methodName == "onSurfaceTextureUpdated" && numArgs == 1)
        {
            auto surface = LocalRef<jobject> (env->GetObjectArrayElement (args, 0));

            owner.onSurfaceTextureUpdated (surface);
            return nullptr;
        }

        return AndroidInterfaceImplementer::invoke (proxy, method, args);
    }

private:
    Owner& owner;
};

//==============================================================================
class ImageReaderOnImageAvailableListener     : public AndroidInterfaceImplementer
{
public:
    struct Owner
    {
        virtual ~Owner() {}

        virtual void onImageAvailable (LocalRef<jobject>& imageReader) = 0;
    };

    ImageReaderOnImageAvailableListener (Owner& ownerToUse)
        : owner (ownerToUse)
    {}

    jobject invoke (jobject proxy, jobject method, jobjectArray args) override
    {
        auto* env = getEnv();

        auto methodName = juceString ((jstring) env->CallObjectMethod (method, JavaMethod.getName));

        int numArgs = args != nullptr ? env->GetArrayLength (args) : 0;

        if (methodName == "onImageAvailable" && numArgs == 1)
        {
            auto imageReader = LocalRef<jobject> (env->GetObjectArrayElement (args, 0));

            owner.onImageAvailable (imageReader);
            return nullptr;
        }

        return AndroidInterfaceImplementer::invoke (proxy, method, args);
    }

private:
    Owner& owner;
};

//==============================================================================
class MediaRecorderOnInfoListener     : public AndroidInterfaceImplementer
{
public:
    struct Owner
    {
        virtual ~Owner() = default;

        virtual void onInfo (LocalRef<jobject>& mediaRecorder, int what, int extra) = 0;
    };

    MediaRecorderOnInfoListener (Owner& ownerToUse)
        : owner (ownerToUse)
    {}

    jobject invoke (jobject proxy, jobject method, jobjectArray args) override
    {
        auto* env = getEnv();

        auto methodName = juceString ((jstring) env->CallObjectMethod (method, JavaMethod.getName));

        int numArgs = args != nullptr ? env->GetArrayLength (args) : 0;

        if (methodName == "onInfo" && numArgs == 3)
        {
            auto mediaRecorder = LocalRef<jobject> (env->GetObjectArrayElement (args, 0));
            auto what   = LocalRef<jobject> (env->GetObjectArrayElement (args, 1));
            auto extra  = LocalRef<jobject> (env->GetObjectArrayElement (args, 2));

            auto whatInt  = (int) env->CallIntMethod (what, JavaInteger.intValue);
            auto extraInt = (int) env->CallIntMethod (extra, JavaInteger.intValue);

            owner.onInfo (mediaRecorder, whatInt, extraInt);
            return nullptr;
        }

        return AndroidInterfaceImplementer::invoke (proxy, method, args);
    }

private:
    Owner& owner;
};

//==============================================================================
class MediaRecorderOnErrorListener     : public AndroidInterfaceImplementer
{
public:
    struct Owner
    {
        virtual ~Owner() {}

        virtual void onError (LocalRef<jobject>& mediaRecorder, int what, int extra) = 0;
    };

    MediaRecorderOnErrorListener (Owner& ownerToUse)
        : owner (ownerToUse)
    {}

    jobject invoke (jobject proxy, jobject method, jobjectArray args) override
    {
        auto* env = getEnv();

        auto methodName = juceString ((jstring) env->CallObjectMethod (method, JavaMethod.getName));

        int numArgs = args != nullptr ? env->GetArrayLength (args) : 0;

        if (methodName == "onError" && numArgs == 3)
        {
            auto mediaRecorder = LocalRef<jobject> (env->GetObjectArrayElement (args, 0));
            auto what   = LocalRef<jobject> (env->GetObjectArrayElement (args, 1));
            auto extra  = LocalRef<jobject> (env->GetObjectArrayElement (args, 2));

            auto whatInt  = (int) env->CallIntMethod (what, JavaInteger.intValue);
            auto extraInt = (int) env->CallIntMethod (extra, JavaInteger.intValue);

            owner.onError (mediaRecorder, whatInt, extraInt);
            return nullptr;
        }

        return AndroidInterfaceImplementer::invoke (proxy, method, args);
    }

private:
    Owner& owner;
};

//==============================================================================
struct CameraDevice::Pimpl
    : private ActivityLifecycleCallbacks
{
    using InternalOpenCameraResultCallback = std::function<void (const String& /*cameraId*/, const String& /*error*/)>;

    Pimpl (CameraDevice& ownerToUse, const String& cameraIdToUse, int /*index*/,
           int minWidthToUse, int minHeightToUse, int maxWidthToUse, int maxHeightToUse,
           bool /*useHighQuality*/)
        : owner (ownerToUse),
          minWidth (minWidthToUse),
          minHeight (minHeightToUse),
          maxWidth (maxWidthToUse),
          maxHeight (maxHeightToUse),
          cameraId (cameraIdToUse),
          activityLifeListener (CreateJavaInterface (this, "android/app/Application$ActivityLifecycleCallbacks")),
          cameraManager (initialiseCameraManager()),
          cameraCharacteristics (initialiseCameraCharacteristics (cameraManager, cameraId)),
          streamConfigurationMap (cameraCharacteristics),
          previewDisplay (streamConfigurationMap.getPreviewBufferSize()),
          deviceOrientationChangeListener (previewDisplay)
    {
        startBackgroundThread();
    }

    ~Pimpl() override
    {
        auto* env = getEnv();

        env->CallVoidMethod (getAppContext().get(), AndroidApplication.unregisterActivityLifecycleCallbacks, activityLifeListener.get());
        activityLifeListener.clear();
    }

    JUCE_DECLARE_WEAK_REFERENCEABLE (Pimpl)

    String getCameraId() const noexcept { return cameraId; }

    void open (InternalOpenCameraResultCallback cameraOpenCallbackToUse)
    {
        cameraOpenCallback = std::move (cameraOpenCallbackToUse);

        // A valid camera open callback must be passed.
        jassert (cameraOpenCallback != nullptr);

        // The same camera can be opened only once!
        jassert (scopedCameraDevice == nullptr);

        if (cameraOpenCallback == nullptr || scopedCameraDevice != nullptr)
            return;

        RuntimePermissions::request (RuntimePermissions::camera,
                                     [safeThis = WeakReference<Pimpl> { this }] (bool granted) mutable
                                     {
                                         if (safeThis != nullptr)
                                             safeThis->continueOpenRequest (granted);
                                     });
    }

    void continueOpenRequest (bool granted)
    {
        if (getAndroidSDKVersion() >= 21)
        {
            if (granted)
            {
                getEnv()->CallVoidMethod (getAppContext().get(), AndroidApplication.registerActivityLifecycleCallbacks, activityLifeListener.get());
                scopedCameraDevice.reset (new ScopedCameraDevice (*this, cameraId, cameraManager, handler, getAutoFocusModeToUse()));
            }
            else
            {
                invokeCameraOpenCallback ("Camera permission not granted");
            }
        }
        else
        {
            invokeCameraOpenCallback ("Camera requires android sdk version 21 or greater");
        }
    }

    bool openedOk() const noexcept { return scopedCameraDevice->openedOk(); }

    void takeStillPicture (std::function<void (const Image&)> pictureTakenCallbackToUse)
    {
        if (pictureTakenCallbackToUse == nullptr || currentCaptureSessionMode == nullptr)
        {
            jassertfalse;
            return;
        }

        if (currentCaptureSessionMode->isVideoRecordSession())
        {
            // Taking still pictures while recording video is not supported on Android.
            jassertfalse;
            return;
        }

        pictureTakenCallback = std::move (pictureTakenCallbackToUse);

        triggerStillPictureCapture();
    }

    void startRecordingToFile (const File& file, int /*quality*/)
    {
        if (! openedOk())
        {
            jassertfalse;
            return;
        }

        if (! previewDisplay.isReady())
        {
            // Did you remember to create and show a preview display?
            jassertfalse;
            return;
        }

        file.deleteFile();
        file.create();
        jassert (file.existsAsFile());

        // MediaRecorder can't handle videos larger than 1080p
        auto videoSize = chooseBestSize (minWidth, minHeight, jmin (maxWidth, 1080), maxHeight,
                                         streamConfigurationMap.getSupportedVideoRecordingOutputSizes());

        mediaRecorder.reset (new MediaRecorder (file.getFullPathName(), videoSize.getWidth(), videoSize.getHeight(),
                                                getCameraSensorOrientation(), getCameraLensFacing()));

        firstRecordedFrameTimeMs = Time::getCurrentTime();

        currentCaptureSessionMode.reset();
        startVideoRecordingMode (*mediaRecorder);
    }

    void stopRecording()
    {
        currentCaptureSessionMode.reset();
        mediaRecorder.reset();

        startPreviewMode (*imageReader);
    }

    Time getTimeOfFirstRecordedFrame() const
    {
        return firstRecordedFrameTimeMs;
    }

    static StringArray getAvailableDevices()
    {
        if (getAndroidSDKVersion() < 21)
            return StringArray(); // Camera requires SDK version 21 or later

        StringArray results;

        auto* env = getEnv();

        auto cameraManagerToUse = initialiseCameraManager();
        auto cameraIdArray = LocalRef<jobjectArray> ((jobjectArray) env->CallObjectMethod (cameraManagerToUse,
                                                                                           CameraManager.getCameraIdList));

        results = javaStringArrayToJuce (cameraIdArray);

        for (auto& result : results)
            printDebugCameraInfo (cameraManagerToUse, result);

        return results;
    }

    void addListener (CameraDevice::Listener* listenerToAdd)
    {
        const ScopedLock sl (listenerLock);
        listeners.add (listenerToAdd);

        if (listeners.size() == 1)
            triggerStillPictureCapture();
    }

    void removeListener (CameraDevice::Listener* listenerToRemove)
    {
        const ScopedLock sl (listenerLock);
        listeners.remove (listenerToRemove);
    }

private:
    enum
    {
        ERROR_CAMERA_IN_USE = 1,
        ERROR_MAX_CAMERAS_IN_USE = 2,
        ERROR_CAMERA_DISABLED = 3,
        ERROR_CAMERA_DEVICE = 4,
        ERROR_CAMERA_SERVICE = 5
    };

    static String cameraErrorCodeToString (int errorCode)
    {
        switch (errorCode)
        {
            case ERROR_CAMERA_IN_USE:      return "Camera already in use.";
            case ERROR_MAX_CAMERAS_IN_USE: return "Too many opened camera devices.";
            case ERROR_CAMERA_DISABLED:    return "Camera disabled.";
            case ERROR_CAMERA_DEVICE:      return "Fatal error.";
            case ERROR_CAMERA_SERVICE:     return "Fatal error. Reboot required or persistent hardware problem.";
            default:                       return "Unknown error.";
        }
    }

    static LocalRef<jobject> initialiseCameraManager()
    {
        return LocalRef<jobject> (getEnv()->CallObjectMethod (getAppContext().get(), AndroidContext.getSystemService,
                                                              javaString ("camera").get()));
    }

    static LocalRef<jobject> initialiseCameraCharacteristics (const GlobalRef& cameraManager, const String& cameraId)
    {
        return LocalRef<jobject> (getEnv()->CallObjectMethod (cameraManager,
                                                              CameraManager.getCameraCharacteristics,
                                                              javaString (cameraId).get()));
    }

    static void printDebugCameraInfo (const LocalRef<jobject>& cameraManagerToUse, const String& cameraId)
    {
        auto* env = getEnv();

        auto characteristics = LocalRef<jobject> (env->CallObjectMethod (cameraManagerToUse,
                                                                         CameraManager.getCameraCharacteristics,
                                                                         javaString (cameraId).get()));

        auto keysList = LocalRef<jobject> (env->CallObjectMethod (characteristics, CameraCharacteristics.getKeys));

        const int size = env->CallIntMethod (keysList, JavaList.size);

        JUCE_CAMERA_LOG ("Camera id: " + cameraId + ", characteristics keys num: " + String (size));

        for (int i = 0; i < size; ++i)
        {
            auto key = LocalRef<jobject> (env->CallObjectMethod (keysList, JavaList.get, i));
            auto jKeyName = LocalRef<jstring> ((jstring) env->CallObjectMethod (key, CameraCharacteristicsKey.getName));
            [[maybe_unused]] auto keyName = juceString (jKeyName);

            auto keyValue = LocalRef<jobject> (env->CallObjectMethod (characteristics, CameraCharacteristics.get, key.get()));
            auto jKeyValueString = LocalRef<jstring> ((jstring) env->CallObjectMethod (keyValue, JavaObject.toString));
            auto keyValueString = juceString (jKeyValueString);

            auto &kvs = keyValueString;

            if (kvs.startsWith ("[I") || kvs.startsWith ("[F") || kvs.startsWith ("[Z") || kvs.startsWith ("[B"))
            {
                printPrimitiveArrayElements (keyValue, keyName, keyValueString);
            }
            else if (kvs.startsWith ("[Landroid.util.Range"))
            {
                printRangeArrayElements (keyValue, keyName);
            }
            else
            {
                int chunkSize = 256;

                if (keyValueString.length() > chunkSize)
                {
                    JUCE_CAMERA_LOG ("Key: " + keyName);

                    for (int j = 0, k = 1; j < keyValueString.length(); j += chunkSize, ++k)
                        JUCE_CAMERA_LOG ("value part " + String (k) + ": " + keyValueString.substring (j, k + chunkSize));
                }
                else
                {
                    JUCE_CAMERA_LOG ("Key: " + keyName + ", value: " + keyValueString);
                }
            }
        }
    }

    static void printPrimitiveArrayElements (const LocalRef<jobject>& keyValue, [[maybe_unused]] const String& keyName,
                                             const String& keyValueString)
    {
        String result = "[";

        auto* env = getEnv();

        #define PRINT_ELEMENTS(elem_type, array_type, fun_name_middle)                                              \
        {                                                                                                           \
            elem_type* elements = env->Get##fun_name_middle##ArrayElements ((array_type) keyValue.get(), nullptr);  \
            int size = env->GetArrayLength ((array_type) keyValue.get());                                           \
                                                                                                                    \
            for (int i = 0; i < size - 1; ++i)                                                                      \
                result << String (elements[i]) << " ";                                                              \
                                                                                                                    \
            if (size > 0)                                                                                           \
                result << String (elements[size - 1]);                                                              \
                                                                                                                    \
            env->Release##fun_name_middle##ArrayElements ((array_type) keyValue.get(), elements, 0);                \
        }

        if (keyValueString.startsWith ("[I"))
            PRINT_ELEMENTS (jint, jintArray, Int)
        else if (keyValueString.startsWith ("[F"))
            PRINT_ELEMENTS (float, jfloatArray, Float)
        else if (keyValueString.startsWith ("[Z"))
            PRINT_ELEMENTS (jboolean, jbooleanArray, Boolean)
        else if (keyValueString.startsWith ("[B"))
            PRINT_ELEMENTS (jbyte, jbyteArray, Byte);

        #undef PRINT_ELEMENTS

        result << "]";
        JUCE_CAMERA_LOG ("Key: " + keyName + ", value: " + result);
    }

    static void printRangeArrayElements (const LocalRef<jobject>& rangeArray, [[maybe_unused]] const String& keyName)
    {
        auto* env = getEnv();

        jobjectArray ranges = static_cast<jobjectArray> (rangeArray.get());

        int numRanges = env->GetArrayLength (ranges);

        String result;

        for (int i = 0; i < numRanges; ++i)
        {
            auto range = LocalRef<jobject> (env->GetObjectArrayElement (ranges, i));

            auto jRangeString = LocalRef<jstring> ((jstring) env->CallObjectMethod (range, AndroidRange.toString));

            result << juceString (jRangeString) << " ";
        }

        JUCE_CAMERA_LOG ("Key: " + keyName + ", value: " + result);
    }

    //==============================================================================
    class StreamConfigurationMap
    {
    public:
        StreamConfigurationMap (const GlobalRef& cameraCharacteristicsToUse)
            : scalerStreamConfigurationMap (getStreamConfigurationMap (cameraCharacteristicsToUse)),
              supportedPreviewOutputSizes (retrieveOutputSizes (scalerStreamConfigurationMap,
                                                                getClassForName ("android.graphics.SurfaceTexture"),
                                                                -1)),
              supportedStillImageOutputSizes (retrieveOutputSizes (scalerStreamConfigurationMap,
                                                                   LocalRef<jobject>(),
                                                                   jpegImageFormat)),
              supportedVideoRecordingOutputSizes (retrieveOutputSizes (scalerStreamConfigurationMap,
                                                                       getClassForName ("android.media.MediaRecorder"),
                                                                       -1)),
              defaultPreviewSize (getSmallestSize (supportedPreviewOutputSizes)),
              previewBufferSize (getLargestSize (supportedPreviewOutputSizes))
        {
            printSizesLog (supportedPreviewOutputSizes, "SurfaceTexture");
            printSizesLog (supportedStillImageOutputSizes, "JPEG");
            printSizesLog (supportedVideoRecordingOutputSizes, "MediaRecorder");
        }

        Array<Rectangle<int>> getSupportedPreviewOutputSizes()        const noexcept { return supportedPreviewOutputSizes; }
        Array<Rectangle<int>> getSupportedStillImageOutputSizes()     const noexcept { return supportedStillImageOutputSizes; }
        Array<Rectangle<int>> getSupportedVideoRecordingOutputSizes() const noexcept { return supportedVideoRecordingOutputSizes; }

        Rectangle<int> getDefaultPreviewSize() const noexcept { return defaultPreviewSize; }
        Rectangle<int> getPreviewBufferSize()  const noexcept { return previewBufferSize; }

        bool isOutputSupportedForSurface (const LocalRef<jobject>& surface) const
        {
            return getEnv()->CallBooleanMethod (scalerStreamConfigurationMap, AndroidStreamConfigurationMap.isOutputSupportedForSurface, surface.get()) != 0;
        }

        static constexpr int jpegImageFormat = 256;

    private:
        GlobalRef scalerStreamConfigurationMap;

        Array<Rectangle<int>> supportedPreviewOutputSizes;
        Array<Rectangle<int>> supportedStillImageOutputSizes;
        Array<Rectangle<int>> supportedVideoRecordingOutputSizes;
        Rectangle<int> defaultPreviewSize, previewBufferSize;

        GlobalRef getStreamConfigurationMap (const GlobalRef& cameraCharacteristicsToUse)
        {
            auto* env = getEnv();

            auto scalerStreamConfigurationMapKey = LocalRef<jobject> (env->GetStaticObjectField (CameraCharacteristics,
                                                                                                 CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP));

            return GlobalRef (LocalRef<jobject> (env->CallObjectMethod (cameraCharacteristicsToUse,
                                                                        CameraCharacteristics.get,
                                                                        scalerStreamConfigurationMapKey.get())));
        }

        static Array<Rectangle<int>> retrieveOutputSizes (GlobalRef& scalerStreamConfigurationMap,
                                                          const LocalRef<jobject>& outputClass,
                                                          int format)
        {
            Array<Rectangle<int>> result;

            auto* env = getEnv();

            auto outputSizes = outputClass.get() != nullptr
                             ? LocalRef<jobjectArray> ((jobjectArray) env->CallObjectMethod (scalerStreamConfigurationMap,
                                                                                             AndroidStreamConfigurationMap.getOutputSizesForClass,
                                                                                             outputClass.get()))
                             : LocalRef<jobjectArray> ((jobjectArray) env->CallObjectMethod (scalerStreamConfigurationMap,
                                                                                             AndroidStreamConfigurationMap.getOutputSizesForFormat,
                                                                                             (jint) format));

            if (format != -1)
            {
                auto supported = (env->CallBooleanMethod (scalerStreamConfigurationMap, AndroidStreamConfigurationMap.isOutputSupportedFor, (jint) format) != 0);

                if (! supported)
                {
                    // The output format is not supported by this device, still image capture will not work!
                    jassertfalse;
                    return {};
                }
            }

            int numSizes = env->GetArrayLength (outputSizes);

            jassert (numSizes > 0);

            for (int i = 0; i < numSizes; ++i)
            {
                auto size = LocalRef<jobject> (env->GetObjectArrayElement (outputSizes, i));

                auto width  = env->CallIntMethod (size, AndroidSize.getWidth);
                auto height = env->CallIntMethod (size, AndroidSize.getHeight);

                result.add (Rectangle<int> (0, 0, width, height));
            }

            return result;
        }

        static LocalRef<jobject> getClassForName (const String& name)
        {
            return LocalRef<jobject> (getEnv()->CallStaticObjectMethod (JavaClass, JavaClass.forName,
                                                                        javaString (name).get()));
        }

        static void printSizesLog ([[maybe_unused]] const Array<Rectangle<int>>& sizes, [[maybe_unused]] const String& className)
        {
            JUCE_CAMERA_LOG ("Sizes for class " + className);

          #if JUCE_CAMERA_LOG_ENABLED
            for (auto& s : sizes)
                JUCE_CAMERA_LOG (s.toString() + "\n");
          #endif
        }

        Rectangle<int> getSmallestSize (const Array<Rectangle<int>>& sizes) const
        {
            if (sizes.size() == 0)
                return {};

            auto smallestSize = sizes[0];

            for (auto& size : sizes)
            {
                if (size.getWidth() < smallestSize.getWidth() && size.getHeight() < smallestSize.getHeight())
                    smallestSize = size;
            }

            return smallestSize;
        }

        Rectangle<int> getLargestSize (const Array<Rectangle<int>>& sizes) const
        {
            if (sizes.size() == 0)
                return {};

            auto largestSize = sizes[0];

            for (auto& size : sizes)
            {
                if (size.getWidth() > largestSize.getWidth() && size.getHeight() > largestSize.getHeight())
                    largestSize = size;
            }

            return largestSize;
        }
    };

    //==============================================================================
    class PreviewDisplay    : private TextureViewSurfaceTextureListener::Owner
    {
    public:
        struct Listener
        {
            virtual ~Listener() {}

            virtual void previewDisplayReady() = 0;
            virtual void previewDisplayAboutToBeDestroyed() = 0;
        };

        PreviewDisplay (Rectangle<int> bufferSize)
            : textureViewSurfaceTextureListener (*this),
              textureView (LocalRef<jobject> (getEnv()->NewObject (AndroidTextureView, AndroidTextureView.constructor,
                                                                   getAppContext().get()))),
              bufferWidth (bufferSize.getWidth()),
              bufferHeight (bufferSize.getHeight())
        {
            auto* env = getEnv();

            if (! isReady())
                env->CallVoidMethod (textureView, AndroidTextureView.setSurfaceTextureListener,
                                     CreateJavaInterface (&textureViewSurfaceTextureListener,
                                                          "android/view/TextureView$SurfaceTextureListener").get());
        }

        ~PreviewDisplay() override
        {
            getEnv()->CallVoidMethod (textureView, AndroidTextureView.setSurfaceTextureListener, nullptr);
        }

        void addListener (Listener* l)
        {
            if (l == nullptr)
            {
                jassertfalse;
                return;
            }

            listeners.add (l);

            if (isReady())
                l->previewDisplayReady();
        }

        void removeListener (Listener* l)
        {
            if (l == nullptr)
            {
                jassertfalse;
                return;
            }

            listeners.remove (l);
        }

        bool isReady() const
        {
            return (getEnv()->CallBooleanMethod (textureView, AndroidTextureView.isAvailable) != 0)
                    && width > 0 && height > 0;
        }

        LocalRef<jobject> createSurface()
        {
            // Surface may get destroyed while session is being configured, if
            // the preview gets hidden in the meantime, so bailout.
            if (! isReady())
                return LocalRef<jobject> (nullptr);

            auto* env = getEnv();

            auto surfaceTexture = LocalRef<jobject> (env->CallObjectMethod (textureView,
                                                                            AndroidTextureView.getSurfaceTexture));

            // NB: too small buffer will result in pixelated preview. A buffer with wrong aspect ratio
            //     can result in a cropped preview.
            env->CallVoidMethod (surfaceTexture, AndroidSurfaceTexture.setDefaultBufferSize, (jint) bufferWidth, (jint) bufferHeight);

            auto surface = LocalRef<jobject> (env->NewObject (AndroidSurface, AndroidSurface.constructor, surfaceTexture.get()));

            return surface;
        }

        const GlobalRef& getNativeView() { return textureView; }

        void updateSurfaceTransform()
        {
            auto* env = getEnv();

            auto windowManager = LocalRef<jobject> (env->CallObjectMethod (getAppContext(), AndroidContext.getSystemService, javaString ("window").get()));
            auto display = LocalRef<jobject> (env->CallObjectMethod (windowManager, AndroidWindowManager.getDefaultDisplay));
            auto rotation = env->CallIntMethod (display, AndroidDisplay.getRotation);

            static constexpr int rotation90 = 1;
            static constexpr int rotation270 = 3;

            auto matrix = LocalRef<jobject> (env->NewObject (AndroidMatrix, AndroidMatrix.constructor));

            if (rotation == rotation90 || rotation == rotation270)
            {
                env->CallBooleanMethod (matrix, AndroidMatrix.postScale, jfloat ((float) height / (float) width), jfloat ((float) width / (float) height), (jfloat) 0, (jfloat) 0);
                env->CallBooleanMethod (matrix, AndroidMatrix.postRotate, (jfloat) 90 * ((float) rotation - 2), (jfloat) 0, (jfloat) 0);
                env->CallBooleanMethod (matrix, AndroidMatrix.postTranslate, (jfloat) (rotation == 3 ? width : 0), (jfloat) (rotation == 1 ? height : 0));
            }

            env->CallVoidMethod (textureView, AndroidTextureView.setTransform, matrix.get());
        }

    private:
        ListenerList<Listener> listeners;

        TextureViewSurfaceTextureListener textureViewSurfaceTextureListener;
        GlobalRef textureView;
        int width = -1, height = -1;
        int bufferWidth, bufferHeight;

        void onSurfaceTextureAvailable (LocalRef<jobject>& /*surface*/, int widthToUse, int heightToUse) override
        {
            JUCE_CAMERA_LOG ("onSurfaceTextureAvailable()");

            width = widthToUse;
            height = heightToUse;

            updateSurfaceTransform();

            listeners.call (&Listener::previewDisplayReady);
        }

        bool onSurfaceTextureDestroyed (LocalRef<jobject>& /*surface*/) override
        {
            JUCE_CAMERA_LOG ("onSurfaceTextureDestroyed()");

            listeners.call (&Listener::previewDisplayAboutToBeDestroyed);

            return true;
        }

        void onSurfaceTextureSizeChanged (LocalRef<jobject>& /*surface*/, int widthToUse, int heightToUse) override
        {
            JUCE_CAMERA_LOG ("onSurfaceTextureSizeChanged()");

            width = widthToUse;
            height = heightToUse;

            updateSurfaceTransform();
        }

        void onSurfaceTextureUpdated (LocalRef<jobject>& /*surface*/) override
        {
            JUCE_CAMERA_LOG ("onSurfaceTextureUpdated()");
        }

        JUCE_DECLARE_NON_COPYABLE (PreviewDisplay)
    };

    //==============================================================================
    class ImageReader   : private ImageReaderOnImageAvailableListener::Owner
    {
    public:
        ImageReader (Pimpl& ownerToUse, GlobalRef& handlerToUse,
                     int imageWidth, int imageHeight, int cameraSensorOrientationToUse)
            : owner (ownerToUse),
              cameraSensorOrientation (cameraSensorOrientationToUse),
              imageReader (LocalRef<jobject> (getEnv()->CallStaticObjectMethod (AndroidImageReader, AndroidImageReader.newInstance,
                                                                                imageWidth, imageHeight, StreamConfigurationMap::jpegImageFormat,
                                                                                numImagesToKeep))),
              onImageAvailableListener (*this)
        {
            getEnv()->CallVoidMethod (imageReader, AndroidImageReader.setOnImageAvailableListener,
                                      CreateJavaInterface (&onImageAvailableListener,
                                                           "android/media/ImageReader$OnImageAvailableListener").get(),
                                      handlerToUse.get());
        }

        ~ImageReader() override
        {
            getEnv()->CallVoidMethod (imageReader, AndroidImageReader.close);
        }

        LocalRef<jobject> getSurface() const
        {
            return LocalRef<jobject> (getEnv()->CallObjectMethod (imageReader, AndroidImageReader.getSurface));
        }

        void resetNotificationFlag()
        {
            hasNotifiedListeners.set (0);
        }

    private:
        Pimpl& owner;
        int cameraSensorOrientation;

        GlobalRef imageReader;
        ImageReaderOnImageAvailableListener onImageAvailableListener;
        static constexpr int numImagesToKeep = 2;
        Atomic<int> hasNotifiedListeners { 0 };

        JUCE_DECLARE_WEAK_REFERENCEABLE (ImageReader)

        void onImageAvailable (LocalRef<jobject>& /*imageReader*/) override
        {
            JUCE_CAMERA_LOG ("onImageAvailable()");

            auto* env = getEnv();

            auto jImage = LocalRef<jobject> (env->CallObjectMethod (imageReader, AndroidImageReader.acquireLatestImage));

            if (jImage.get() == nullptr)
                return;

            auto cameraLensFrontFacing = owner.getCameraLensFacing() == 0;

            // NB: could use sensor orientation here to get real-world orientation, but then the resulting
            //     image could not match the UI orientation.
            auto image = androidImageToJuceWithFixedOrientation (jImage, owner.deviceOrientationChangeListener.getDeviceOrientation(),
                                                                 Desktop::getInstance().getCurrentOrientation(),
                                                                 cameraLensFrontFacing,
                                                                 cameraSensorOrientation);

            env->CallVoidMethod (jImage, AndroidImage.close);

            owner.callListeners (image);

            // Android may take multiple pictures before it handles a request to stop.
            if (hasNotifiedListeners.compareAndSetBool (1, 0))
                MessageManager::callAsync ([safeThis = WeakReference<ImageReader> { this }, image]() mutable
                {
                    if (safeThis != nullptr)
                        safeThis->owner.notifyPictureTaken (image);
                });
        }

        struct ImageBuffer
        {
            LocalRef<jbyteArray> byteArray;
            int size;
        };

        static Image androidImageToJuceWithFixedOrientation (const LocalRef<jobject>& androidImage,
                                                             Desktop::DisplayOrientation deviceOrientationFromAccelerometerSensor,
                                                             Desktop::DisplayOrientation targetOrientation,
                                                             bool cameraLensFrontFacing,
                                                             int cameraSensorOrientation)
        {
            auto* env = getEnv();

            auto planes = LocalRef<jobjectArray> ((jobjectArray) env->CallObjectMethod (androidImage, AndroidImage.getPlanes));
            jassert (env->GetArrayLength (planes) > 0);

            auto plane = LocalRef<jobject> (env->GetObjectArrayElement (planes, 0));
            auto byteBuffer = LocalRef<jobject> (env->CallObjectMethod (plane, AndroidImagePlane.getBuffer));

            ImageBuffer correctedBuffer = getImageBufferWithCorrectedOrientationFrom (byteBuffer, deviceOrientationFromAccelerometerSensor,
                                                                                      targetOrientation, cameraLensFrontFacing, cameraSensorOrientation);

            jbyte* rawBytes = env->GetByteArrayElements (correctedBuffer.byteArray, nullptr);

            Image result = ImageFileFormat::loadFrom (rawBytes, (size_t) correctedBuffer.size);

            env->ReleaseByteArrayElements (correctedBuffer.byteArray, rawBytes, 0);

            return result;
        }

        static ImageBuffer getImageBufferWithCorrectedOrientationFrom (const LocalRef<jobject>& imagePlaneBuffer,
                                                                       Desktop::DisplayOrientation deviceOrientationFromAccelerometerSensor,
                                                                       Desktop::DisplayOrientation targetOrientation,
                                                                       bool cameraLensFrontFacing,
                                                                       int cameraSensorOrientation)
        {
            auto* env = getEnv();

            auto bufferSize = env->CallIntMethod (imagePlaneBuffer, JavaByteBuffer.remaining);
            auto byteArray = LocalRef<jbyteArray> (env->NewByteArray (bufferSize));
            env->CallObjectMethod (imagePlaneBuffer, JavaByteBuffer.get, byteArray.get());

            auto rotationAngle = getRotationAngle (deviceOrientationFromAccelerometerSensor, targetOrientation,
                                                  cameraLensFrontFacing, cameraSensorOrientation);

            if (rotationAngle == 0)
            {
                // Nothing to do, just get the bytes
                return { byteArray, bufferSize };
            }

            auto origBitmap = LocalRef<jobject> (env->CallStaticObjectMethod (AndroidBitmapFactory,
                                                                              AndroidBitmapFactory.decodeByteArray,
                                                                              byteArray.get(), (jint) 0, (jint) bufferSize));

            if (origBitmap == nullptr)
            {
                // Nothing to do, just get the bytes
                return { byteArray, bufferSize };
            }

            auto correctedBitmap = getBitmapWithCorrectOrientationFrom (origBitmap, rotationAngle);

            auto byteArrayOutputStream = LocalRef<jobject> (env->NewObject (ByteArrayOutputStream,
                                                                            ByteArrayOutputStream.constructor));

            auto jCompressFormatString = javaString ("JPEG");
            auto compressFormat = LocalRef<jobject> (env->CallStaticObjectMethod (AndroidBitmapCompressFormat,
                                                                                  AndroidBitmapCompressFormat.valueOf,
                                                                                  jCompressFormatString.get()));

            if (env->CallBooleanMethod (correctedBitmap, AndroidBitmap.compress, compressFormat.get(),
                                        (jint) 100, byteArrayOutputStream.get()) != 0)
            {
                auto correctedByteArray = LocalRef<jbyteArray> ((jbyteArray) env->CallObjectMethod (byteArrayOutputStream,
                                                                                                    ByteArrayOutputStream.toByteArray));

                int correctedByteArraySize = env->CallIntMethod (byteArrayOutputStream, ByteArrayOutputStream.size);

                return { correctedByteArray, correctedByteArraySize };
            }

            jassertfalse;
            // fallback, return original bitmap
            return { byteArray, bufferSize };
        }

        static int getRotationAngle (Desktop::DisplayOrientation deviceOrientationFromAccelerometerSensor,
                                     Desktop::DisplayOrientation targetOrientation,
                                     bool cameraLensFrontFacing,
                                     int cameraSensorOrientation)
        {
            auto isSensorOrientationHorizontal = deviceOrientationFromAccelerometerSensor == Desktop::rotatedAntiClockwise
                                              || deviceOrientationFromAccelerometerSensor == Desktop::rotatedClockwise;

            if (cameraLensFrontFacing && isSensorOrientationHorizontal)
            {
                // flip angles for front camera
                return getRotationAngle (deviceOrientationFromAccelerometerSensor, targetOrientation, false, (cameraSensorOrientation + 180) % 360);
            }

            switch (targetOrientation)
            {
                case Desktop::rotatedAntiClockwise:
                    return cameraSensorOrientation == 90 ? 0 : 180;
                case Desktop::rotatedClockwise:
                    return cameraSensorOrientation == 90 ? 180 : 0;
                case Desktop::upright:
                case Desktop::upsideDown:
                    if ((targetOrientation == Desktop::upright && ! cameraLensFrontFacing)
                        || (targetOrientation == Desktop::upsideDown && cameraLensFrontFacing))
                    {
                        return cameraSensorOrientation;
                    }
                    else
                    {
                        if (deviceOrientationFromAccelerometerSensor == Desktop::upright || deviceOrientationFromAccelerometerSensor == Desktop::upsideDown)
                            return cameraSensorOrientation;
                        else
                            return (cameraSensorOrientation + 180) % 360;
                    }
                    break;
                case Desktop::allOrientations:
                default:
                    return 0;
            }
        }

        static LocalRef<jobject> getBitmapWithCorrectOrientationFrom (LocalRef<jobject>& origBitmap, int rotationAngle)
        {
            auto* env = getEnv();

            auto origBitmapWidth  = env->CallIntMethod (origBitmap, AndroidBitmap.getWidth);
            auto origBitmapHeight = env->CallIntMethod (origBitmap, AndroidBitmap.getHeight);

            auto matrix = LocalRef<jobject> (env->NewObject (AndroidMatrix, AndroidMatrix.constructor));
            env->CallBooleanMethod (matrix, AndroidMatrix.postRotate, (jfloat) rotationAngle, (jfloat) 0, (jfloat) 0);

            auto rotatedBitmap = LocalRef<jobject> (env->CallStaticObjectMethod (AndroidBitmap, AndroidBitmap.createBitmapFrom,
                                                                                 origBitmap.get(), (jint) 0, (jint) 0,
                                                                                 (jint) origBitmapWidth, (jint) origBitmapHeight,
                                                                                 matrix.get(), true));

            env->CallVoidMethod (origBitmap, AndroidBitmap.recycle);

            return rotatedBitmap;
        }
    };

    //==============================================================================
    class MediaRecorder : private MediaRecorderOnInfoListener::Owner,
                          private MediaRecorderOnErrorListener::Owner
    {
    public:
        MediaRecorder (const String& outputFilePath, int videoWidth, int videoHeight,
                       int sensorOrientation, int cameraLensFacing)
            : onInfoListener (*this),
              onErrorListener (*this),
              mediaRecorder (LocalRef<jobject> (getEnv()->NewObject (AndroidMediaRecorder,
                                                                     AndroidMediaRecorder.constructor)))
        {
            auto* env = getEnv();

            env->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.setOnInfoListener,
                                 CreateJavaInterface (&onInfoListener,
                                                      "android/media/MediaRecorder$OnInfoListener").get());

            env->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.setOnErrorListener,
                                 CreateJavaInterface (&onErrorListener,
                                                      "android/media/MediaRecorder$OnErrorListener").get());

            // NB: the order of function calls here is enforced, and exceptions will be thrown if
            //     the order is changed.
            static constexpr int audioSourceMic = 1;
            env->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.setAudioSource, (jint) audioSourceMic);

            static constexpr int videoSourceSurface = 2;
            env->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.setVideoSource, (jint) videoSourceSurface);

            static constexpr int outputFormatMPEG4 = 2;
            env->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.setOutputFormat, (jint) outputFormatMPEG4);

            static constexpr int audioEncoderAAC = 3;
            env->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.setAudioEncoder, (jint) audioEncoderAAC);

            static constexpr int videoEncoderH264 = 2;
            env->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.setVideoEncoder, (jint) videoEncoderH264);

            env->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.setVideoEncodingBitRate, (jint) 10000000);
            env->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.setVideoFrameRate, (jint) 30);

            auto frontFacing = cameraLensFacing == 0;

            auto useInverseDegrees = frontFacing && sensorOrientation == 90;

            int orientationHint = getOrientationHint (useInverseDegrees, sensorOrientation);
            env->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.setOrientationHint, (jint) orientationHint);

            getEnv()->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.setVideoSize, (jint) videoWidth, (jint) videoHeight);
            getEnv()->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.setOutputFile, javaString (outputFilePath).get());
            getEnv()->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.prepare);
        }

        ~MediaRecorder() override
        {
            getEnv()->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.release);
        }

        LocalRef<jobject> getSurface()
        {
            return LocalRef<jobject> (getEnv()->CallObjectMethod (mediaRecorder, AndroidMediaRecorder.getSurface));
        }

        void start()
        {
            lockScreenOrientation();

            getEnv()->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.start);

            hasStartedRecording = true;
        }

        void stop()
        {
            // A request to stop can be sent before recording has had a chance to start, so
            // ignore the request rather than calling AndroidMediaRecorder.stop because
            // otherwise MediaRecorder will throw an exception and...
            if (! hasStartedRecording)
                return;

            hasStartedRecording = false;

            auto* env = getEnv();
            env->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.stop);

            // ... ignore RuntimeException that can be thrown if stop() was called after recording
            // has started but before any frame was written to a file. This is not an error.
            jniCheckHasExceptionOccurredAndClear();

            unlockScreenOrientation();
        }

    private:
        MediaRecorderOnInfoListener onInfoListener;
        MediaRecorderOnErrorListener onErrorListener;
        GlobalRef mediaRecorder;
        bool hasStartedRecording = false;
        int orientationsEnabled = -1;

        void lockScreenOrientation()
        {
            orientationsEnabled = Desktop::getInstance().getOrientationsEnabled();

            auto o = Desktop::getInstance().getCurrentOrientation();
            Desktop::getInstance().setOrientationsEnabled (o);
        }

        static jint juceOrientationToNativeOrientation (int orientations) noexcept
        {
            enum
            {
                SCREEN_ORIENTATION_LANDSCAPE          = 0,
                SCREEN_ORIENTATION_PORTRAIT           = 1,
                SCREEN_ORIENTATION_USER               = 2,
                SCREEN_ORIENTATION_REVERSE_LANDSCAPE  = 8,
                SCREEN_ORIENTATION_REVERSE_PORTRAIT   = 9,
                SCREEN_ORIENTATION_USER_LANDSCAPE     = 11,
                SCREEN_ORIENTATION_USER_PORTRAIT      = 12,
            };

            switch (orientations)
            {
                case Desktop::upright:                                          return (jint) SCREEN_ORIENTATION_PORTRAIT;
                case Desktop::upsideDown:                                       return (jint) SCREEN_ORIENTATION_REVERSE_PORTRAIT;
                case Desktop::upright + Desktop::upsideDown:                    return (jint) SCREEN_ORIENTATION_USER_PORTRAIT;
                case Desktop::rotatedAntiClockwise:                             return (jint) SCREEN_ORIENTATION_LANDSCAPE;
                case Desktop::rotatedClockwise:                                 return (jint) SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
                case Desktop::rotatedClockwise + Desktop::rotatedAntiClockwise: return (jint) SCREEN_ORIENTATION_USER_LANDSCAPE;
                default:                                                        return (jint) SCREEN_ORIENTATION_USER;
            }
        }

        void unlockScreenOrientation()
        {
            Desktop::getInstance().setOrientationsEnabled (orientationsEnabled);
        }

        void onInfo ([[maybe_unused]] LocalRef<jobject>& recorder, [[maybe_unused]] int what, [[maybe_unused]] int extra) override
        {
            JUCE_CAMERA_LOG ("MediaRecorder::OnInfo: " + getInfoStringFromCode (what)
                                     + ", extra code = " + String (extra));
        }

        void onError ([[maybe_unused]] LocalRef<jobject>& recorder, [[maybe_unused]] int what, [[maybe_unused]] int extra) override
        {
            JUCE_CAMERA_LOG ("MediaRecorder::onError: " + getErrorStringFromCode (what)
                                     + ", extra code = " + String (extra));
        }

        static String getInfoStringFromCode (int what)
        {
            enum
            {
                MEDIA_RECORDER_INFO_UNKNOWN = 1,
                MEDIA_RECORDER_INFO_MAX_DURATION_REACHED = 800,
                MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED = 801,
                MEDIA_RECORDER_INFO_MAX_FILESIZE_APPROACHING = 802,
                MEDIA_RECORDER_INFO_NEXT_OUTPUT_FILE_STARTED = 803
            };

            switch (what)
            {
                case MEDIA_RECORDER_INFO_UNKNOWN:                  return { "Unknown info" };
                case MEDIA_RECORDER_INFO_MAX_DURATION_REACHED:     return { "Max duration reached" };
                case MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED:     return { "Max filesize reached" };
                case MEDIA_RECORDER_INFO_MAX_FILESIZE_APPROACHING: return { "Max filesize approaching" };
                case MEDIA_RECORDER_INFO_NEXT_OUTPUT_FILE_STARTED: return { "Next output file started" };
                default: return String (what);
            };
        }

        static String getErrorStringFromCode (int what)
        {
            enum
            {
                MEDIA_RECORDER_ERROR_UNKNOWN = 1,
                MEDIA_ERROR_SERVER_DIED = 100
            };

            switch (what)
            {
                case MEDIA_RECORDER_ERROR_UNKNOWN:   return { "Unknown error" };
                case MEDIA_ERROR_SERVER_DIED:        return { "Server died" };
                default: return String (what);
            };
        }

        static int getOrientationHint (bool useInverseDegrees, int cameraSensorOrientation)
        {
            auto* env = getEnv();

            auto windowManager = LocalRef<jobject> (env->CallObjectMethod (getAppContext(), AndroidContext.getSystemService, javaString ("window").get()));
            auto display = LocalRef<jobject> (env->CallObjectMethod (windowManager, AndroidWindowManager.getDefaultDisplay));
            auto rotation = env->CallIntMethod (display, AndroidDisplay.getRotation);

            enum
            {
                ROTATION_0 = 0,
                ROTATION_90,
                ROTATION_180,
                ROTATION_270
            };

            int hint = 0;

            switch (rotation)
            {
                case ROTATION_0:   hint = cameraSensorOrientation;       break;
                case ROTATION_90:  hint = useInverseDegrees ? 180 : 0;   break;
                case ROTATION_180: hint = cameraSensorOrientation + 180; break;
                case ROTATION_270: hint = useInverseDegrees ? 0 : 180;   break;
                default: jassertfalse;
            }

            return (hint + 360) % 360;
        }
    };

    //==============================================================================
    class ScopedCameraDevice
    {
    public:
        //==============================================================================
        class CaptureSession
        {
        public:
            struct ConfiguredCallback
            {
                virtual ~ConfiguredCallback() {}

                virtual void captureSessionConfigured (CaptureSession*) = 0;
            };

            ~CaptureSession()
            {
                bool calledClose = false;

                auto* env = getEnv();

                {
                    const ScopedLock lock (captureSessionLock);

                    if (captureSession.get() != nullptr)
                    {
                        calledClose = true;

                        env->CallVoidMethod (captureSession, CameraCaptureSession.close);
                    }
                }

                // When exception occurs, CameraCaptureSession.close will never finish, so
                // we should not wait for it. For fatal error an exception does occur, but
                // it is caught internally in Java...
                if (jniCheckHasExceptionOccurredAndClear() || scopedCameraDevice.fatalErrorOccurred.get())
                {
                    JUCE_CAMERA_LOG ("Exception or fatal error occurred while closing Capture Session, closing by force");
                }
                else if (calledClose)
                {
                    pendingClose.set (1);
                    closedEvent.wait (-1);
                }
            }

            bool openedOk() const noexcept { return captureSession != nullptr; }

            const GlobalRef& getNativeSession() const { return captureSession; }

            bool start (const LocalRef<jobject>& targetSurfacesList, GlobalRef& handlerToUse)
            {
                if (! openedOk())
                {
                    jassertfalse;
                    return false;
                }

                auto* env = getEnv();

                auto numSurfaces = env->CallIntMethod (targetSurfacesList, JavaArrayList.size);

                for (int i = 0; i < numSurfaces; ++i)
                {
                    auto surface = LocalRef<jobject> (env->CallObjectMethod (targetSurfacesList, JavaArrayList.get, (jint) i));
                    env->CallVoidMethod (captureRequestBuilder, CaptureRequestBuilder.addTarget, surface.get());
                }

                previewCaptureRequest = GlobalRef (LocalRef<jobject> (env->CallObjectMethod (captureRequestBuilder, CaptureRequestBuilder.build)));

                env->CallIntMethod (captureSession, CameraCaptureSession.setRepeatingRequest,
                                    previewCaptureRequest.get(), nullptr, handlerToUse.get());

                return true;
            }

            void takeStillPicture (jobject targetSurface)
            {
                if (stillPictureTaker == nullptr)
                {
                    // Can only take picture once session was successfully configured!
                    jassertfalse;
                    return;
                }

                auto* env = getEnv();

                static constexpr int templateStillCapture = 2;
                auto builder = LocalRef<jobject> (env->CallObjectMethod (scopedCameraDevice.cameraDevice,
                                                                         AndroidCameraDevice.createCaptureRequest,
                                                                         (jint) templateStillCapture));

                env->CallVoidMethod (builder, CaptureRequestBuilder.addTarget, targetSurface);

                setCaptureRequestBuilderIntegerKey (builder, CaptureRequest.CONTROL_AF_MODE, autoFocusMode);

                auto stillPictureCaptureRequest = LocalRef<jobject> (env->CallObjectMethod (builder, CaptureRequestBuilder.build));

                stillPictureTaker->takePicture (stillPictureCaptureRequest);
            }

        private:
            //==============================================================================
            class StillPictureTaker   : private AndroidRunnable::Owner
            {
            public:
                StillPictureTaker (GlobalRef& captureSessionToUse, GlobalRef& captureRequestBuilderToUse,
                                   GlobalRef& previewCaptureRequestToUse, GlobalRef& handlerToUse,
                                   int autoFocusModeToUse)
                    : captureSession (captureSessionToUse),
                      captureRequestBuilder (captureRequestBuilderToUse),
                      previewCaptureRequest (previewCaptureRequestToUse),
                      handler (handlerToUse),
                      runnable (*this),
                      captureSessionPreviewCaptureCallback (createCaptureSessionCallback (true)),
                      captureSessionStillPictureCaptureCallback (createCaptureSessionCallback (false)),
                      autoFocusMode (autoFocusModeToUse)
                {
                }

                void takePicture (const LocalRef<jobject>& stillPictureCaptureRequestToUse)
                {
                    JUCE_CAMERA_LOG ("Taking picture...");

                    stillPictureCaptureRequest = GlobalRef (LocalRef<jobject> (stillPictureCaptureRequestToUse));

                    lockFocus();
                }

            private:
                GlobalRef& captureSession;
                GlobalRef& captureRequestBuilder;
                GlobalRef& previewCaptureRequest;
                GlobalRef& handler;

                AndroidRunnable runnable;
                GlobalRef delayedCaptureRunnable;

                GlobalRef captureSessionPreviewCaptureCallback;

                GlobalRef stillPictureCaptureRequest;
                GlobalRef captureSessionStillPictureCaptureCallback;

                int autoFocusMode;

                //==============================================================================
                #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
                   METHOD (constructor, "<init>", "(JZ)V")              \
                   CALLBACK (generatedCallback<&StillPictureTaker::cameraCaptureSessionCaptureCompletedCallback>,         "cameraCaptureSessionCaptureCompleted",         "(JZLandroid/hardware/camera2/CameraCaptureSession;Landroid/hardware/camera2/CaptureRequest;Landroid/hardware/camera2/TotalCaptureResult;)V") \
                   CALLBACK (generatedCallback<&StillPictureTaker::cameraCaptureSessionCaptureFailedCallback>,            "cameraCaptureSessionCaptureFailed",            "(JZLandroid/hardware/camera2/CameraCaptureSession;Landroid/hardware/camera2/CaptureRequest;Landroid/hardware/camera2/CaptureFailure;)V") \
                   CALLBACK (generatedCallback<&StillPictureTaker::cameraCaptureSessionCaptureProgressedCallback>,        "cameraCaptureSessionCaptureProgressed",        "(JZLandroid/hardware/camera2/CameraCaptureSession;Landroid/hardware/camera2/CaptureRequest;Landroid/hardware/camera2/CaptureResult;)V") \
                   CALLBACK (generatedCallback<&StillPictureTaker::cameraCaptureSessionCaptureStartedCallback>,           "cameraCaptureSessionCaptureStarted",           "(JZLandroid/hardware/camera2/CameraCaptureSession;Landroid/hardware/camera2/CaptureRequest;JJ)V") \
                   CALLBACK (generatedCallback<&StillPictureTaker::cameraCaptureSessionCaptureSequenceAbortedCallback>,   "cameraCaptureSessionCaptureSequenceAborted",   "(JZLandroid/hardware/camera2/CameraCaptureSession;I)V") \
                   CALLBACK (generatedCallback<&StillPictureTaker::cameraCaptureSessionCaptureSequenceCompletedCallback>, "cameraCaptureSessionCaptureSequenceCompleted", "(JZLandroid/hardware/camera2/CameraCaptureSession;IJ)V")

                DECLARE_JNI_CLASS_WITH_BYTECODE (CameraCaptureSessionCaptureCallback, "com/rmsl/juce/CameraCaptureSessionCaptureCallback", 21, CameraSupportByteCode)
                #undef JNI_CLASS_MEMBERS

                LocalRef<jobject> createCaptureSessionCallback (bool createPreviewSession)
                {
                    return LocalRef<jobject> (getEnv()->NewObject (CameraCaptureSessionCaptureCallback,
                                                                   CameraCaptureSessionCaptureCallback.constructor,
                                                                   reinterpret_cast<jlong> (this),
                                                                   createPreviewSession ? 1 : 0));
                }

                //==============================================================================
                enum class State
                {
                    idle = 0,
                    pendingFocusLock,
                    pendingExposurePrecapture,
                    pendingExposurePostPrecapture,
                    pictureTaken
                };

                State currentState = State::idle;

                void lockFocus()
                {
                    if (jniCheckHasExceptionOccurredAndClear())
                        return;

                    JUCE_CAMERA_LOG ("Performing auto-focus if possible...");

                    currentState = State::pendingFocusLock;

                    auto* env = getEnv();

                    // NB: auto-focus may be unavailable on a device, in which case it may have already
                    // automatically adjusted the exposure. We check for that in updateState().
                    static constexpr int controlAfTriggerStart = 1;
                    CaptureSession::setCaptureRequestBuilderIntegerKey (captureRequestBuilder.get(),
                                                                        CaptureRequest.CONTROL_AF_TRIGGER,
                                                                        controlAfTriggerStart);

                    auto previewRequest = LocalRef<jobject> (env->CallObjectMethod (captureRequestBuilder,
                                                                                    CaptureRequestBuilder.build));

                    env->CallIntMethod (captureSession, CameraCaptureSession.capture, previewRequest.get(),
                                        captureSessionPreviewCaptureCallback.get(), handler.get());
                }

                void updateState (jobject captureResult)
                {
                    // IllegalStateException can be thrown when accessing CaptureSession,
                    // claiming that capture session was already closed but we may not
                    // get relevant callback yet, so check for this and bailout when needed.
                    if (jniCheckHasExceptionOccurredAndClear())
                        return;

                    switch (currentState)
                    {
                        case State::pendingFocusLock:
                        {
                            JUCE_CAMERA_LOG ("Still picture capture, updateState(), State::pendingFocusLock...");

                            auto controlAfStateValue = getCaptureResultIntegerKeyValue (CaptureResult.CONTROL_AF_STATE, captureResult);

                            if (controlAfStateValue.get() == nullptr)
                            {
                                captureStillPictureDelayed();
                                return;
                            }

                            auto autoToFocusNotAvailable = autoFocusMode == 0;

                            if (autoToFocusNotAvailable || autoFocusHasFinished (controlAfStateValue))
                            {
                                auto controlAeStateIntValue = getControlAEState (captureResult);
                                static constexpr int controlAeStateConverged = 2;

                                if (controlAeStateIntValue == -1 || controlAeStateIntValue == controlAeStateConverged)
                                {
                                    currentState = State::pictureTaken;
                                    captureStillPictureDelayed();
                                }
                                else
                                {
                                    runPrecaptureSequence();
                                }
                            }

                            break;
                        }

                        case State::pendingExposurePrecapture:
                        {
                            JUCE_CAMERA_LOG ("Still picture capture, updateState(), State::pendingExposurePrecapture...");

                            auto controlAeStateIntValue = getControlAEState (captureResult);
                            static constexpr int controlAeStateFlashRequired = 4;
                            static constexpr int controlAeStatePrecapture = 5;

                            if (controlAeStateIntValue == -1 || controlAeStateIntValue == controlAeStateFlashRequired
                                                             || controlAeStateIntValue == controlAeStatePrecapture)
                            {
                                currentState = State::pendingExposurePostPrecapture;
                            }

                            break;
                        }

                        case State::pendingExposurePostPrecapture:
                        {
                            JUCE_CAMERA_LOG ("Still picture capture, updateState(), State::pendingExposurePostPrecapture...");

                            auto controlAeStateIntValue = getControlAEState (captureResult);
                            static constexpr int controlAeStatePrecapture = 5;

                            if (controlAeStateIntValue == -1 || controlAeStateIntValue != controlAeStatePrecapture)
                            {
                                currentState = State::pictureTaken;
                                captureStillPictureDelayed();
                            }

                            break;
                        }
                        case State::idle:
                        case State::pictureTaken:
                            { /* do nothing */ break; }
                    };
                }

                static int getControlAEState (jobject captureResult)
                {
                    auto controlAeStateValue = getCaptureResultIntegerKeyValue (CaptureResult.CONTROL_AE_STATE, captureResult);

                    return controlAeStateValue.get() != nullptr
                                    ? getEnv()->CallIntMethod (controlAeStateValue, JavaInteger.intValue) : -1;
                }

                static bool autoFocusHasFinished (const LocalRef<jobject>& controlAfStateValue)
                {
                    static constexpr int controlAfStateFocusedLocked = 4;
                    static constexpr int controlAfStateNotFocusedLocked = 5;

                    auto controlAfStateIntValue = getEnv()->CallIntMethod (controlAfStateValue, JavaInteger.intValue);

                    return controlAfStateIntValue == controlAfStateFocusedLocked || controlAfStateIntValue == controlAfStateNotFocusedLocked;
                }

                static LocalRef<jobject> getCaptureResultIntegerKeyValue (jfieldID key, jobject captureResult)
                {
                    auto* env = getEnv();

                    auto jKey = LocalRef<jobject> (env->GetStaticObjectField (CaptureResult, key));
                    return LocalRef<jobject> (env->CallObjectMethod (captureResult, CaptureResult.get, jKey.get()));
                }

                void captureStillPictureDelayed()
                {
                    if (jniCheckHasExceptionOccurredAndClear())
                        return;

                    JUCE_CAMERA_LOG ("Still picture capture, device ready, capturing now...");

                    auto* env = getEnv();

                    env->CallVoidMethod (captureSession, CameraCaptureSession.stopRepeating);

                    if (jniCheckHasExceptionOccurredAndClear())
                        return;

                    env->CallVoidMethod (captureSession, CameraCaptureSession.abortCaptures);

                    if (jniCheckHasExceptionOccurredAndClear())
                        return;

                    // Delay still picture capture for devices that can't handle it right after
                    // stopRepeating/abortCaptures calls.
                    if (delayedCaptureRunnable.get() == nullptr)
                        delayedCaptureRunnable = GlobalRef (CreateJavaInterface (&runnable, "java/lang/Runnable"));

                    env->CallBooleanMethod (handler, AndroidHandler.postDelayed, delayedCaptureRunnable.get(), (jlong) 200);
                }

                void runPrecaptureSequence()
                {
                    if (jniCheckHasExceptionOccurredAndClear())
                        return;

                    auto* env = getEnv();

                    static constexpr int controlAePrecaptureTriggerStart = 1;
                    CaptureSession::setCaptureRequestBuilderIntegerKey (captureRequestBuilder.get(),
                                                                        CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
                                                                        controlAePrecaptureTriggerStart);

                    currentState = State::pendingExposurePrecapture;

                    auto previewRequest = LocalRef<jobject> (env->CallObjectMethod (captureRequestBuilder,
                                                                                    CaptureRequestBuilder.build));

                    env->CallIntMethod (captureSession, CameraCaptureSession.capture, previewRequest.get(),
                                        captureSessionPreviewCaptureCallback.get(), handler.get());
                }

                void unlockFocus()
                {
                    if (jniCheckHasExceptionOccurredAndClear())
                        return;

                    JUCE_CAMERA_LOG ("Unlocking focus...");

                    currentState = State::idle;

                    auto* env = getEnv();

                    static constexpr int controlAfTriggerCancel = 2;
                    CaptureSession::setCaptureRequestBuilderIntegerKey (captureRequestBuilder.get(),
                                                                        CaptureRequest.CONTROL_AF_TRIGGER,
                                                                        controlAfTriggerCancel);

                    auto resetAutoFocusRequest = LocalRef<jobject> (env->CallObjectMethod (captureRequestBuilder,
                                                                                           CaptureRequestBuilder.build));

                    env->CallIntMethod (captureSession, CameraCaptureSession.capture, resetAutoFocusRequest.get(),
                                        nullptr, handler.get());

                    if (jniCheckHasExceptionOccurredAndClear())
                        return;

                    // NB: for preview, using preview capture request again
                    env->CallIntMethod (captureSession, CameraCaptureSession.setRepeatingRequest, previewCaptureRequest.get(),
                                        nullptr, handler.get());
                }
                //==============================================================================
                void run() override
                {
                    captureStillPicture();
                }

                void captureStillPicture()
                {
                    getEnv()->CallIntMethod (captureSession, CameraCaptureSession.capture,
                                             stillPictureCaptureRequest.get(), captureSessionStillPictureCaptureCallback.get(),
                                             nullptr);
                }

                //==============================================================================
                void cameraCaptureSessionCaptureCompleted (bool isPreview,
                                                           [[maybe_unused]] jobject session,
                                                           [[maybe_unused]] jobject request,
                                                           jobject result)
                {
                    JUCE_CAMERA_LOG ("cameraCaptureSessionCaptureCompleted()");

                    if (isPreview)
                        updateState (result);
                    else if (currentState != State::idle)
                        unlockFocus();
                }

                void cameraCaptureSessionCaptureFailed ([[maybe_unused]] bool isPreview,
                                                        [[maybe_unused]] jobject session,
                                                        [[maybe_unused]] jobject request,
                                                        [[maybe_unused]] jobject failure)
                {
                    JUCE_CAMERA_LOG ("cameraCaptureSessionCaptureFailed()");
                }

                void cameraCaptureSessionCaptureProgressed (bool isPreview,
                                                            [[maybe_unused]] jobject session,
                                                            [[maybe_unused]] jobject request,
                                                            jobject partialResult)
                {
                    JUCE_CAMERA_LOG ("cameraCaptureSessionCaptureProgressed()");

                    if (isPreview)
                        updateState (partialResult);
                }

                void cameraCaptureSessionCaptureSequenceAborted ([[maybe_unused]] bool isPreview,
                                                                 [[maybe_unused]] jobject session,
                                                                 [[maybe_unused]] int sequenceId)
                {
                    JUCE_CAMERA_LOG ("cameraCaptureSessionCaptureSequenceAborted()");
                }

                void cameraCaptureSessionCaptureSequenceCompleted ([[maybe_unused]] bool isPreview,
                                                                   [[maybe_unused]] jobject session,
                                                                   [[maybe_unused]] int sequenceId,
                                                                   [[maybe_unused]] int64 frameNumber)
                {
                    JUCE_CAMERA_LOG ("cameraCaptureSessionCaptureSequenceCompleted()");
                }

                void cameraCaptureSessionCaptureStarted ([[maybe_unused]] bool isPreview,
                                                         [[maybe_unused]] jobject session,
                                                         [[maybe_unused]] jobject request,
                                                         [[maybe_unused]] int64 timestamp,
                                                         [[maybe_unused]] int64 frameNumber)
                {
                    JUCE_CAMERA_LOG ("cameraCaptureSessionCaptureStarted()");
                }

                //==============================================================================
                static void cameraCaptureSessionCaptureCompletedCallback (JNIEnv* env, StillPictureTaker& t, jboolean isPreview, jobject rawSession, jobject rawRequest, jobject rawResult)
                {
                    LocalRef<jobject> session (env->NewLocalRef (rawSession));
                    LocalRef<jobject> request (env->NewLocalRef (rawRequest));
                    LocalRef<jobject> result (env->NewLocalRef (rawResult));

                    t.cameraCaptureSessionCaptureCompleted (isPreview != 0, session, request, result);
                }

                static void cameraCaptureSessionCaptureFailedCallback (JNIEnv* env, StillPictureTaker& t, jboolean isPreview, jobject rawSession, jobject rawRequest, jobject rawResult)
                {
                    LocalRef<jobject> session (env->NewLocalRef (rawSession));
                    LocalRef<jobject> request (env->NewLocalRef (rawRequest));
                    LocalRef<jobject> result (env->NewLocalRef (rawResult));

                    t.cameraCaptureSessionCaptureFailed (isPreview != 0, session, request, result);
                }

                static void cameraCaptureSessionCaptureProgressedCallback (JNIEnv* env, StillPictureTaker& t, jboolean isPreview, jobject rawSession, jobject rawRequest, jobject rawResult)
                {
                    LocalRef<jobject> session (env->NewLocalRef (rawSession));
                    LocalRef<jobject> request (env->NewLocalRef (rawRequest));
                    LocalRef<jobject> result (env->NewLocalRef (rawResult));

                    t.cameraCaptureSessionCaptureProgressed (isPreview != 0, session, request, result);
                }

                static void cameraCaptureSessionCaptureSequenceAbortedCallback (JNIEnv* env, StillPictureTaker& t, jboolean isPreview, jobject rawSession, jint sequenceId)
                {
                    LocalRef<jobject> session (env->NewLocalRef (rawSession));

                    t.cameraCaptureSessionCaptureSequenceAborted (isPreview != 0, session, sequenceId);
                }

                static void cameraCaptureSessionCaptureSequenceCompletedCallback (JNIEnv* env, StillPictureTaker& t, jboolean isPreview, jobject rawSession, jint sequenceId, jlong frameNumber)
                {
                    LocalRef<jobject> session (env->NewLocalRef (rawSession));

                    t.cameraCaptureSessionCaptureSequenceCompleted (isPreview != 0, session, sequenceId, frameNumber);
                }

                static void cameraCaptureSessionCaptureStartedCallback (JNIEnv* env, StillPictureTaker& t, jboolean isPreview, jobject rawSession, jobject rawRequest, jlong timestamp, jlong frameNumber)
                {
                    LocalRef<jobject> session (env->NewLocalRef (rawSession));
                    LocalRef<jobject> request (env->NewLocalRef (rawRequest));

                    t.cameraCaptureSessionCaptureStarted (isPreview != 0, session, request, timestamp, frameNumber);
                }
            };

            //==============================================================================
            ScopedCameraDevice& scopedCameraDevice;
            ConfiguredCallback& configuredCallback;
            GlobalRef& handler;

            GlobalRef captureRequestBuilder;
            GlobalRef previewCaptureRequest;

            GlobalRef captureSessionStateCallback;
            int autoFocusMode;

            GlobalRef captureSession;
            CriticalSection captureSessionLock;

            Atomic<int> pendingClose { 0 };

            std::unique_ptr<StillPictureTaker> stillPictureTaker;

            WaitableEvent closedEvent;

            JUCE_DECLARE_WEAK_REFERENCEABLE (CaptureSession)

            //==============================================================================
            #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK)    \
                METHOD (constructor, "<init>", "(J)V") \
                CALLBACK (generatedCallback<&CaptureSession::cameraCaptureSessionActiveCallback>,          "cameraCaptureSessionActive",          "(JLandroid/hardware/camera2/CameraCaptureSession;)V") \
                CALLBACK (generatedCallback<&CaptureSession::cameraCaptureSessionClosedCallback>,          "cameraCaptureSessionClosed",          "(JLandroid/hardware/camera2/CameraCaptureSession;)V") \
                CALLBACK (generatedCallback<&CaptureSession::cameraCaptureSessionConfigureFailedCallback>, "cameraCaptureSessionConfigureFailed", "(JLandroid/hardware/camera2/CameraCaptureSession;)V") \
                CALLBACK (generatedCallback<&CaptureSession::cameraCaptureSessionConfiguredCallback>,      "cameraCaptureSessionConfigured",      "(JLandroid/hardware/camera2/CameraCaptureSession;)V") \
                CALLBACK (generatedCallback<&CaptureSession::cameraCaptureSessionReadyCallback>,           "cameraCaptureSessionReady",           "(JLandroid/hardware/camera2/CameraCaptureSession;)V")

            DECLARE_JNI_CLASS_WITH_MIN_SDK (CameraCaptureSessionStateCallback, "com/rmsl/juce/CameraCaptureSessionStateCallback", 21)
            #undef JNI_CLASS_MEMBERS


            //==============================================================================
            CaptureSession (ScopedCameraDevice& scopedCameraDeviceToUse, ConfiguredCallback& configuredCallbackToUse,
                            const LocalRef<jobject>& surfacesList, GlobalRef& handlerToUse,
                            int captureSessionTemplate, int autoFocusModeToUse)
                : scopedCameraDevice (scopedCameraDeviceToUse),
                  configuredCallback (configuredCallbackToUse),
                  handler (handlerToUse),
                  captureRequestBuilder (LocalRef<jobject> (getEnv()->CallObjectMethod (scopedCameraDevice.cameraDevice,
                                                                                        AndroidCameraDevice.createCaptureRequest,
                                                                                        (jint) captureSessionTemplate))),
                  captureSessionStateCallback (LocalRef<jobject> (getEnv()->NewObject (CameraCaptureSessionStateCallback,
                                                                                       CameraCaptureSessionStateCallback.constructor,
                                                                                       reinterpret_cast<jlong> (this)))),
                  autoFocusMode (autoFocusModeToUse)
            {
                auto* env = getEnv();

                env->CallVoidMethod (scopedCameraDevice.cameraDevice, AndroidCameraDevice.createCaptureSession,
                                     surfacesList.get(), captureSessionStateCallback.get(), handler.get());

                static constexpr int controlModeAuto = 1;
                setCaptureRequestBuilderIntegerKey (captureRequestBuilder.get(), CaptureRequest.CONTROL_MODE, controlModeAuto);

                setCaptureRequestBuilderIntegerKey (captureRequestBuilder.get(), CaptureRequest.CONTROL_AF_MODE, autoFocusMode);
            }

            static void setCaptureRequestBuilderIntegerKey (jobject captureRequestBuilder, jfieldID key, int value)
            {
                auto* env = getEnv();

                auto jKey = LocalRef<jobject> (env->GetStaticObjectField (CaptureRequest, key));
                auto jValue = LocalRef<jobject> (env->CallStaticObjectMethod (JavaInteger, JavaInteger.valueOf, (jint) value));

                env->CallVoidMethod (captureRequestBuilder, CaptureRequestBuilder.set, jKey.get(), jValue.get());
            }

            //==============================================================================
            static void cameraCaptureSessionActiveCallback ([[maybe_unused]] JNIEnv* env,
                                                            [[maybe_unused]] CaptureSession& t,
                                                            [[maybe_unused]] jobject rawSession)
            {
                JUCE_CAMERA_LOG ("cameraCaptureSessionActive()");
            }

            static void cameraCaptureSessionClosedCallback ([[maybe_unused]] JNIEnv* env,
                                                            CaptureSession& t,
                                                            [[maybe_unused]] jobject rawSession)
            {
                JUCE_CAMERA_LOG ("cameraCaptureSessionClosed()");

                t.closedEvent.signal();
            }

            static void cameraCaptureSessionConfigureFailedCallback ([[maybe_unused]] JNIEnv* env,
                                                                     CaptureSession& t,
                                                                     [[maybe_unused]] jobject rawSession)
            {
                JUCE_CAMERA_LOG ("cameraCaptureSessionConfigureFailed()");

                MessageManager::callAsync ([weakRef = WeakReference<CaptureSession> { &t }]
                                           {
                                               if (weakRef != nullptr)
                                                   weakRef->configuredCallback.captureSessionConfigured (nullptr);
                                           });
            }

            static void cameraCaptureSessionConfiguredCallback (JNIEnv* env, CaptureSession& t, jobject rawSession)
            {
                LocalRef<jobject> session (env->NewLocalRef (rawSession));
                JUCE_CAMERA_LOG ("cameraCaptureSessionConfigured()");

                if (t.pendingClose.get() == 1)
                {
                    // Already closing, bailout.
                    t.closedEvent.signal();

                    GlobalRef s (session);

                    MessageManager::callAsync ([s]()
                                               {
                                                   getEnv()->CallVoidMethod (s, CameraCaptureSession.close);
                                               });

                    return;
                }

                {
                    const ScopedLock lock (t.captureSessionLock);
                    t.captureSession = GlobalRef (session);
                }

                MessageManager::callAsync ([weakRef = WeakReference<CaptureSession> { &t }]
                                           {
                                               if (weakRef == nullptr)
                                                   return;

                                               weakRef->stillPictureTaker.reset (new StillPictureTaker (weakRef->captureSession,
                                                                                                        weakRef->captureRequestBuilder,
                                                                                                        weakRef->previewCaptureRequest,
                                                                                                        weakRef->handler,
                                                                                                        weakRef->autoFocusMode));

                                               weakRef->configuredCallback.captureSessionConfigured (weakRef.get());
                                           });
            }

            static void cameraCaptureSessionReadyCallback ([[maybe_unused]] JNIEnv* env,
                                                           [[maybe_unused]] CaptureSession& t,
                                                           [[maybe_unused]] jobject rawSession)
            {
                JUCE_CAMERA_LOG ("cameraCaptureSessionReady()");
            }

            //==============================================================================
            friend class ScopedCameraDevice;

            JUCE_DECLARE_NON_COPYABLE (CaptureSession)
        };

        //==============================================================================
        ScopedCameraDevice (Pimpl& ownerToUse, const String& cameraIdToUse, GlobalRef& cameraManagerToUse,
                            GlobalRef& handlerToUse, int autoFocusModeToUse)
            : owner (ownerToUse),
              cameraId (cameraIdToUse),
              cameraManager (cameraManagerToUse),
              handler (handlerToUse),
              cameraStateCallback (createCameraStateCallbackObject()),
              autoFocusMode (autoFocusModeToUse)
        {
            open();
        }

        ~ScopedCameraDevice()
        {
            close();
        }

        void open()
        {
            pendingOpen.set (1);

            auto* env = getEnv();

            env->CallVoidMethod (cameraManager, CameraManager.openCamera,
                                 javaString (cameraId).get(),
                                 cameraStateCallback.get(), handler.get());

            // If something went wrong we will be pinged in cameraDeviceStateError()
            // callback, silence the redundant exception.
            jniCheckHasExceptionOccurredAndClear();
        }

        void close()
        {
            if (pendingClose.compareAndSetBool (1, 0))
            {
                auto* env = getEnv();

                if (cameraDevice.get() != nullptr)
                {
                    env->CallVoidMethod (cameraDevice, AndroidCameraDevice.close);
                    closedEvent.wait (-1);
                }

                pendingClose.set (0);
                pendingOpen .set (0);
                cameraDevice.clear();
            }
        }

        bool openedOk() const { return cameraDevice != nullptr; }

        bool hasErrorOccurred() const { return fatalErrorOccurred.get(); }

        CaptureSession* createCaptureSession (CaptureSession::ConfiguredCallback& cc,
                                              const LocalRef<jobject>& surfacesList,
                                              GlobalRef& handlerToUse,
                                              int captureSessionTemplate)
        {
            if (! openedOk())
            {
                jassertfalse;
                return nullptr;
            }

            return new CaptureSession (*this, cc, surfacesList, handlerToUse, captureSessionTemplate, autoFocusMode);
        }

    private:
        Pimpl& owner;
        const String cameraId;
        GlobalRef& cameraManager;
        GlobalRef& handler;

        GlobalRef cameraStateCallback;
        int autoFocusMode;

        GlobalRef cameraDevice;
        Atomic<int> pendingOpen { 0 };
        Atomic<int> pendingClose { 0 };
        Atomic<int> fatalErrorOccurred { 0 };
        String openError;

        WaitableEvent closedEvent;

        //==============================================================================
        #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK)    \
            METHOD (constructor, "<init>", "(J)V") \
            CALLBACK (generatedCallback<&ScopedCameraDevice::cameraDeviceStateClosedCallback>,       "cameraDeviceStateClosed",       "(JLandroid/hardware/camera2/CameraDevice;)V")  \
            CALLBACK (generatedCallback<&ScopedCameraDevice::cameraDeviceStateDisconnectedCallback>, "cameraDeviceStateDisconnected", "(JLandroid/hardware/camera2/CameraDevice;)V")  \
            CALLBACK (generatedCallback<&ScopedCameraDevice::cameraDeviceStateErrorCallback>,        "cameraDeviceStateError",        "(JLandroid/hardware/camera2/CameraDevice;I)V") \
            CALLBACK (generatedCallback<&ScopedCameraDevice::cameraDeviceStateOpenedCallback>,       "cameraDeviceStateOpened",       "(JLandroid/hardware/camera2/CameraDevice;)V")

        DECLARE_JNI_CLASS_WITH_MIN_SDK (CameraDeviceStateCallback, "com/rmsl/juce/CameraDeviceStateCallback", 21)
        #undef JNI_CLASS_MEMBERS

        LocalRef<jobject> createCameraStateCallbackObject()
        {
            return LocalRef<jobject> (getEnv()->NewObject (CameraDeviceStateCallback,
                                                           CameraDeviceStateCallback.constructor,
                                                           reinterpret_cast<jlong> (this)));
        }

        //==============================================================================
        void notifyOpenResult()
        {
            MessageManager::callAsync ([this]() { owner.cameraOpenFinished (openError); });
        }

        //==============================================================================
        static void cameraDeviceStateClosedCallback (JNIEnv*, ScopedCameraDevice& s, jobject)
        {
            JUCE_CAMERA_LOG ("cameraDeviceStateClosed()");

            s.closedEvent.signal();
        }

        static void cameraDeviceStateDisconnectedCallback (JNIEnv*, ScopedCameraDevice& s, jobject)
        {
            JUCE_CAMERA_LOG ("cameraDeviceStateDisconnected()");

            if (s.pendingOpen.compareAndSetBool (0, 1))
            {
                s.openError = "Device disconnected";

                s.notifyOpenResult();
            }

            MessageManager::callAsync ([&s] { s.close(); });
        }

        static void cameraDeviceStateErrorCallback (JNIEnv*, ScopedCameraDevice& s, jobject, jint errorCode)
        {
            auto error = cameraErrorCodeToString (errorCode);

            JUCE_CAMERA_LOG ("cameraDeviceStateError(), error: " + error);

            if (s.pendingOpen.compareAndSetBool (0, 1))
            {
                s.openError = error;

                s.notifyOpenResult();
            }

            s.fatalErrorOccurred.set (1);

            MessageManager::callAsync ([&s, error]()
                                       {
                                           s.owner.cameraDeviceError (error);
                                           s.close();
                                       });
        }

        static void cameraDeviceStateOpenedCallback (JNIEnv* env, ScopedCameraDevice& s, jobject cameraDeviceToUse)
        {
            JUCE_CAMERA_LOG ("cameraDeviceStateOpened()");

            LocalRef<jobject> camera (env->NewLocalRef (cameraDeviceToUse));

            s.pendingOpen.set (0);

            s.cameraDevice = GlobalRef (camera);

            s.notifyOpenResult();
        }
    };

    //==============================================================================
    struct CaptureSessionModeBase
    {
        virtual ~CaptureSessionModeBase() = default;

        virtual bool isVideoRecordSession() const = 0;

        virtual void triggerStillPictureCapture() = 0;
    };

    //==============================================================================
    template <typename Mode>
    struct CaptureSessionMode   : public CaptureSessionModeBase,
                                  private PreviewDisplay::Listener,
                                  private ScopedCameraDevice::CaptureSession::ConfiguredCallback
    {
        ~CaptureSessionMode() override
        {
            captureSession.reset();

            previewDisplay.removeListener (this);
        }

        bool isVideoRecordSession() const override
        {
            return Mode::isVideoRecord();
        }

        void triggerStillPictureCapture() override
        {
            if (captureSession == nullptr)
            {
                // The capture session must be ready before taking a still picture.
                // Did you remember to create and show a preview display?
                jassertfalse;
                return;
            }

            crtp().takeStillPicture();
        }

    protected:
        CaptureSessionMode (Pimpl& ownerToUse, ScopedCameraDevice& cameraDeviceToUse,
                            GlobalRef& handlerToUse, PreviewDisplay& pd, int cameraSensorOrientationToUse,
                            int cameraLensFacingToUse, StreamConfigurationMap& streamConfigurationMapToUse)
            : owner (ownerToUse),
              scopedCameraDevice (cameraDeviceToUse),
              handler (handlerToUse),
              previewDisplay (pd),
              cameraSensorOrientation (cameraSensorOrientationToUse),
              cameraLensFacing (cameraLensFacingToUse),
              streamConfigurationMap (streamConfigurationMapToUse)
        {
            // async so that the object is fully constructed before the callback gets invoked
            MessageManager::callAsync ([weakRef = WeakReference<CaptureSessionMode<Mode>> { this }]
            {
                if (weakRef != nullptr)
                    weakRef->previewDisplay.addListener (weakRef.get());
            });
        }

        Mode& crtp() { return static_cast<Mode&> (*this); }

        void previewDisplayReady() override
        {
            jassert (previewDisplay.isReady());

            JUCE_CAMERA_LOG ("previewDisplayReady()");

            // close previous capture session first
            captureSession.reset();

            if (scopedCameraDevice.hasErrorOccurred())
            {
                JUCE_CAMERA_LOG ("Device error detected, not recreating a new camera session. The device needs to be reopened.");
                return;
            }

            captureSession.reset (scopedCameraDevice.createCaptureSession (*this, crtp().getCaptureSessionSurfaces(),
                                                                           handler, Mode::getTemplate()));
        }

        void previewDisplayAboutToBeDestroyed() override
        {
            JUCE_CAMERA_LOG ("previewDisplayAboutToBeDestroyed()");

            stopPreview();
        }

        void captureSessionConfigured (ScopedCameraDevice::CaptureSession* session) override
        {
            if (session == nullptr)
            {
                owner.cameraDeviceError ("Failed to configure camera session.");
                return;
            }

            jassert (session == captureSession.get());

            startSession();
        }

        void startSession()
        {
            if (! captureSession->start (crtp().getTargetSurfaces(), handler))
            {
                jassertfalse;
                JUCE_CAMERA_LOG ("Could not start capture session");
            }

            crtp().sessionStarted();
        }

        void stopPreview()
        {
            if (captureSession != nullptr)
            {
                auto session = captureSession->getNativeSession();

                auto* env = getEnv();

                env->CallVoidMethod (session, CameraCaptureSession.stopRepeating);

                if (jniCheckHasExceptionOccurredAndClear())
                    return;

                env->CallVoidMethod (session, CameraCaptureSession.abortCaptures);

                jniCheckHasExceptionOccurredAndClear();
            }
        }

        Pimpl& owner;
        ScopedCameraDevice& scopedCameraDevice;
        GlobalRef& handler;
        PreviewDisplay& previewDisplay;
        int cameraSensorOrientation;
        int cameraLensFacing;
        StreamConfigurationMap& streamConfigurationMap;

        std::unique_ptr<ScopedCameraDevice::CaptureSession> captureSession;

        JUCE_DECLARE_WEAK_REFERENCEABLE (CaptureSessionMode<Mode>)
    };

    //==============================================================================
    struct CaptureSessionPreviewMode : public CaptureSessionMode<CaptureSessionPreviewMode>
    {
        CaptureSessionPreviewMode (Pimpl& ownerToUse, ScopedCameraDevice& cameraDeviceToUse, GlobalRef& handlerToUse,
                                   PreviewDisplay& pd, ImageReader& ir, int sensorOrientation,
                                   int cameraLensFacingToUse, StreamConfigurationMap& streamConfigurationMapToUse)
            : CaptureSessionMode<CaptureSessionPreviewMode> (ownerToUse, cameraDeviceToUse, handlerToUse, pd,
                                                             sensorOrientation, cameraLensFacingToUse, streamConfigurationMapToUse),
              imageReader (ir)
        {
        }

        // Surfaces passed to newly created capture session.
        LocalRef<jobject> getCaptureSessionSurfaces() const
        {
            auto* env = getEnv();

            auto previewSurface = LocalRef<jobject> (previewDisplay.createSurface());
            auto imageSurface = LocalRef<jobject> (imageReader.getSurface());

            auto arrayList = LocalRef<jobject> (env->NewObject (JavaArrayList, JavaArrayList.constructor, 2));
            env->CallBooleanMethod (arrayList, JavaArrayList.add, previewSurface.get());
            env->CallBooleanMethod (arrayList, JavaArrayList.add, imageSurface.get());

            auto supported = streamConfigurationMap.isOutputSupportedForSurface (imageSurface);

            // Output surface is not supported by this device, still image capture will not work!
            jassert (supported);

            return arrayList;
        }

        // Surfaces set as target during capture.
        LocalRef<jobject> getTargetSurfaces() const
        {
            auto* env = getEnv();

            auto previewSurface = LocalRef<jobject> (previewDisplay.createSurface());

            auto arrayList = LocalRef<jobject> (env->NewObject (JavaArrayList, JavaArrayList.constructor, 1));
            env->CallBooleanMethod (arrayList, JavaArrayList.add, previewSurface.get());

            return arrayList;
        }

        static int getTemplate()
        {
            static constexpr int templatePreview = 1;
            return templatePreview;
        }

        static bool isVideoRecord() { return false; }

        void sessionStarted() {}

        void takeStillPicture()
        {
            imageReader.resetNotificationFlag();
            captureSession->takeStillPicture (imageReader.getSurface());
        }

    private:
        ImageReader& imageReader;
    };

    //==============================================================================
    struct CaptureSessionVideoRecordingMode : public CaptureSessionMode<CaptureSessionVideoRecordingMode>
    {
        CaptureSessionVideoRecordingMode (Pimpl& ownerToUse, ScopedCameraDevice& cameraDeviceToUse, GlobalRef& handlerToUse,
                                          PreviewDisplay& pd, MediaRecorder& mr, int sensorOrientation,
                                          int cameraLensFacingToUse, StreamConfigurationMap& streamConfigurationMapToUse)
            : CaptureSessionMode<CaptureSessionVideoRecordingMode> (ownerToUse, cameraDeviceToUse, handlerToUse, pd,
                                                                    sensorOrientation, cameraLensFacingToUse, streamConfigurationMapToUse),
              mediaRecorder (mr)
        {
        }

        ~CaptureSessionVideoRecordingMode()
        {
            // We need to explicitly stop the preview before stopping the media recorder,
            // because legacy devices can't handle recording stop before stopping the preview.
            stopPreview();

            mediaRecorder.stop();
        }

        // Surfaces passed to newly created capture session.
        LocalRef<jobject> getCaptureSessionSurfaces() const
        {
            auto* env = getEnv();

            auto previewSurface = LocalRef<jobject> (previewDisplay.createSurface());
            auto mediaRecorderSurface = LocalRef<jobject> (mediaRecorder.getSurface());

            auto arrayList = LocalRef<jobject> (env->NewObject (JavaArrayList, JavaArrayList.constructor, 2));
            env->CallBooleanMethod (arrayList, JavaArrayList.add, previewSurface.get());
            env->CallBooleanMethod (arrayList, JavaArrayList.add, mediaRecorderSurface.get());

            return arrayList;
        }

        // Surfaces set as target during capture.
        LocalRef<jobject> getTargetSurfaces() const
        {
            // Same surfaces used.
            return getCaptureSessionSurfaces();
        }

        static int getTemplate()
        {
            static constexpr int templateRecord = 3;
            return templateRecord;
        }

        static bool isVideoRecord() { return true; }

        void sessionStarted()
        {
            MessageManager::callAsync ([this]() { mediaRecorder.start(); });
        }

        void takeStillPicture()
        {
            // Taking still pictures while recording video is not supported on Android.
            jassertfalse;
        }

    private:
        MediaRecorder& mediaRecorder;
    };

    //==============================================================================
    class DeviceOrientationChangeListener   : private Timer
    {
    public:
        DeviceOrientationChangeListener (PreviewDisplay& pd)
            : previewDisplay (pd),
              orientationEventListener (createOrientationEventListener()),
              canDetectChange (getEnv()->CallBooleanMethod (orientationEventListener,
                                                            OrientationEventListener.canDetectOrientation) != 0),
              deviceOrientation (Desktop::getInstance().getCurrentOrientation()),
              lastKnownScreenOrientation (deviceOrientation)
        {
            setEnabled (true);
        }

        ~DeviceOrientationChangeListener() override
        {
            setEnabled (false);
        }

        void setEnabled (bool shouldBeEnabled)
        {
            if (shouldBeEnabled && ! canDetectChange)
            {
                // This device does not support orientation listening, photos may have wrong orientation!
                jassertfalse;
                return;
            }

            if (shouldBeEnabled)
                getEnv()->CallVoidMethod (orientationEventListener, OrientationEventListener.enable);
            else
                getEnv()->CallVoidMethod (orientationEventListener, OrientationEventListener.disable);
        }

        bool isSupported() const noexcept { return canDetectChange; }

        Desktop::DisplayOrientation getDeviceOrientation() const noexcept
        {
            return deviceOrientation;
        }

    private:
        PreviewDisplay& previewDisplay;

        GlobalRef orientationEventListener;
        static constexpr jint sensorDelayUI = 2;

        bool canDetectChange;
        Desktop::DisplayOrientation deviceOrientation;

        Desktop::DisplayOrientation lastKnownScreenOrientation;
        int numChecksForOrientationChange = 10;

        //==============================================================================
        #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
            METHOD (canDetectOrientation, "canDetectOrientation", "()Z") \
            METHOD (constructor,          "<init>",               "(JLandroid/content/Context;I)V") \
            METHOD (disable,              "disable",              "()V") \
            METHOD (enable,               "enable",               "()V") \
            CALLBACK (generatedCallback<&DeviceOrientationChangeListener::orientationChanged>, "deviceOrientationChanged", "(JI)V")

        DECLARE_JNI_CLASS_WITH_MIN_SDK (OrientationEventListener, "com/rmsl/juce/JuceOrientationEventListener", 21)
        #undef JNI_CLASS_MEMBERS

        LocalRef<jobject> createOrientationEventListener()
        {
            return LocalRef<jobject> (getEnv()->NewObject (OrientationEventListener,
                                                           OrientationEventListener.constructor,
                                                           reinterpret_cast<jlong> (this),
                                                           getAppContext().get(),
                                                           sensorDelayUI));
        }

        //==============================================================================
        static void orientationChanged (JNIEnv*, DeviceOrientationChangeListener& t, jint orientation)
        {
            jassert (orientation < 360);

            // -1 == unknown
            if (orientation < 0)
                return;

            const auto oldOrientation = t.deviceOrientation;

            t.deviceOrientation = [orientation]
            {
                if (orientation > (360 - 45) || orientation < 45)
                    return Desktop::upright;

                if (orientation < 135)
                    return Desktop::rotatedClockwise;

                if (orientation < 225)
                    return Desktop::upsideDown;

                return Desktop::rotatedAntiClockwise;
            }();

            // NB: this assumes natural position to be portrait always, but some devices may be landscape...

            if (oldOrientation != t.deviceOrientation)
            {
                t.lastKnownScreenOrientation = Desktop::getInstance().getCurrentOrientation();

                // Need to update preview transform, but screen orientation will change slightly
                // later than sensor orientation.
                t.startTimer (500);
            }
        }

        void timerCallback() override
        {
            auto currentOrientation = Desktop::getInstance().getCurrentOrientation();

            if (lastKnownScreenOrientation != currentOrientation)
            {
                lastKnownScreenOrientation = currentOrientation;

                stopTimer();
                numChecksForOrientationChange = 10;
                previewDisplay.updateSurfaceTransform();

                return;
            }

            if (--numChecksForOrientationChange == 0)
            {
                stopTimer();
                numChecksForOrientationChange = 10;
            }
        }
    };

    //==============================================================================
    CameraDevice& owner;
    int minWidth, minHeight, maxWidth, maxHeight;

    String cameraId;
    InternalOpenCameraResultCallback cameraOpenCallback;

    GlobalRef activityLifeListener;

    GlobalRef cameraManager;
    GlobalRef cameraCharacteristics;
    GlobalRef handlerThread;
    GlobalRef handler;

    StreamConfigurationMap streamConfigurationMap;
    PreviewDisplay previewDisplay;
    DeviceOrientationChangeListener deviceOrientationChangeListener;
    std::unique_ptr<ImageReader> imageReader;
    std::unique_ptr<MediaRecorder> mediaRecorder;

    std::unique_ptr<CaptureSessionModeBase> currentCaptureSessionMode;

    std::unique_ptr<ScopedCameraDevice> scopedCameraDevice;

    CriticalSection listenerLock;
    ListenerList<Listener> listeners;

    std::function<void (const Image&)> pictureTakenCallback;

    Time firstRecordedFrameTimeMs;
    bool notifiedOfCameraOpening = false;

    bool appWasPaused = false;

    //==============================================================================
    int getCameraSensorOrientation() const
    {
        return getCameraCharacteristicsIntegerKeyValue (CameraCharacteristics.SENSOR_ORIENTATION);
    }

    int getAutoFocusModeToUse() const
    {
        auto supportedModes = getSupportedAutoFocusModes();

        enum
        {
            CONTROL_AF_MODE_OFF = 0,
            CONTROL_AF_MODE_AUTO = 1,
            CONTROL_AF_MODE_CONTINUOUS_PICTURE = 4
        };

        if (supportedModes.contains (CONTROL_AF_MODE_CONTINUOUS_PICTURE))
            return CONTROL_AF_MODE_CONTINUOUS_PICTURE;

        if (supportedModes.contains (CONTROL_AF_MODE_AUTO))
            return CONTROL_AF_MODE_AUTO;

        return CONTROL_AF_MODE_OFF;
    }

    Array<int> getSupportedAutoFocusModes() const
    {
        auto* env = getEnv();

        auto jKey = LocalRef<jobject> (env->GetStaticObjectField (CameraCharacteristics, CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES));

        auto supportedModes = LocalRef<jintArray> ((jintArray) env->CallObjectMethod (cameraCharacteristics,
                                                                                      CameraCharacteristics.get,
                                                                                      jKey.get()));

        return jintArrayToJuceArray (supportedModes);
    }

    static Array<int> jintArrayToJuceArray (const LocalRef<jintArray>& jArray)
    {
        auto* env = getEnv();

        auto* jArrayElems = env->GetIntArrayElements (jArray, nullptr);
        auto numElems = env->GetArrayLength (jArray);

        Array<int> juceArray;

        for (int s = 0; s < numElems; ++s)
            juceArray.add (jArrayElems[s]);

        env->ReleaseIntArrayElements (jArray, jArrayElems, 0);
        return juceArray;
    }

    int getCameraCharacteristicsIntegerKeyValue (jfieldID key) const
    {
        auto* env = getEnv();

        auto jKey = LocalRef<jobject> (env->GetStaticObjectField (CameraCharacteristics, key));

        auto jValue = LocalRef<jobject> (env->CallObjectMethod (cameraCharacteristics,
                                                                CameraCharacteristics.get,
                                                                jKey.get()));

        return env->CallIntMethod (jValue, JavaInteger.intValue);
    }

    int getCameraLensFacing() const
    {
        return getCameraCharacteristicsIntegerKeyValue (CameraCharacteristics.LENS_FACING);
    }

    //==============================================================================
    void cameraOpenFinished (const String& error)
    {
        JUCE_CAMERA_LOG ("cameraOpenFinished(), error = " + error);

        if (error.isEmpty())
        {
            setupStillImageSize();
            startPreviewMode (*imageReader);
        }

        // Do not notify about camera being reopened on app resume.
        if (! notifiedOfCameraOpening)
        {
            notifiedOfCameraOpening = true;

            invokeCameraOpenCallback (error);
        }
    }

    void cameraDeviceError (const String& error)
    {
        NullCheckedInvocation::invoke (owner.onErrorOccurred, error);
    }

    void invokeCameraOpenCallback (const String& error)
    {
        JUCE_CAMERA_LOG ("invokeCameraOpenCallback(), error = " + error);

        NullCheckedInvocation::invoke (cameraOpenCallback, cameraId, error);
    }

    //==============================================================================
    void callListeners (const Image& image)
    {
        const ScopedLock sl (listenerLock);
        listeners.call ([=] (Listener& l) { l.imageReceived (image); });
    }

    void notifyPictureTaken (const Image& image)
    {
        JUCE_CAMERA_LOG ("notifyPictureTaken()");

        NullCheckedInvocation::invoke (pictureTakenCallback, image);
    }

    void triggerStillPictureCapture()
    {
        currentCaptureSessionMode->triggerStillPictureCapture();
    }

    //==============================================================================
    void setupStillImageSize()
    {
        imageReader.reset();

        auto imageSize = chooseBestSize (minWidth, minHeight, maxWidth, maxHeight,
                                         streamConfigurationMap.getSupportedStillImageOutputSizes());

        imageReader.reset (new ImageReader (*this, handler, imageSize.getWidth(), imageSize.getHeight(),
                                            getCameraSensorOrientation()));
    }

    static Rectangle<int> chooseBestSize (int minWidth, int minHeight, int maxWidth, int maxHeight,
                                          Array<Rectangle<int>> supportedSizes)
    {
        Rectangle<int> result;

        for (auto& size : supportedSizes)
        {
            auto width  = size.getWidth();
            auto height = size.getHeight();

            if (width < minWidth || width > maxWidth || height < minHeight || height > maxHeight)
                continue;

            if (size.contains (result))
                result = size;
        }

        // None of the supported sizes matches required width & height limitations, picking
        // the first one available...
        jassert (! result.isEmpty());

        if (result.isEmpty())
            result = supportedSizes[0];

        return result;
    }

    //==============================================================================
    void startPreviewMode (ImageReader& ir)
    {
        if (currentCaptureSessionMode != nullptr && ! currentCaptureSessionMode->isVideoRecordSession())
            return;

        // previous mode has to be stopped first
        jassert (currentCaptureSessionMode.get() == nullptr);

        if (scopedCameraDevice == nullptr || ! scopedCameraDevice->openedOk())
            return;

        currentCaptureSessionMode.reset (new CaptureSessionPreviewMode (*this, *scopedCameraDevice, handler,
                                                                        previewDisplay, ir,
                                                                        getCameraSensorOrientation(),
                                                                        getCameraLensFacing(),
                                                                        streamConfigurationMap));
    }

    void startVideoRecordingMode (MediaRecorder& mr)
    {
        if (currentCaptureSessionMode != nullptr && currentCaptureSessionMode->isVideoRecordSession())
            return;

        // previous mode has to be stopped first
        jassert (currentCaptureSessionMode.get() == nullptr);

        jassert (scopedCameraDevice != nullptr && scopedCameraDevice->openedOk());

        if (scopedCameraDevice == nullptr || ! scopedCameraDevice->openedOk())
            return;

        currentCaptureSessionMode.reset (new CaptureSessionVideoRecordingMode (*this, *scopedCameraDevice, handler,
                                                                               previewDisplay, mr,
                                                                               getCameraSensorOrientation(),
                                                                               getCameraLensFacing(),
                                                                               streamConfigurationMap));
    }

    //==============================================================================
    void onActivityPaused (jobject) override
    {
        JUCE_CAMERA_LOG ("appPaused, closing camera...");

        appWasPaused = true;

        deviceOrientationChangeListener.setEnabled (false);

        // We need to restart the whole session mode when the app gets resumed.
        currentCaptureSessionMode.reset();

        if (scopedCameraDevice != nullptr)
            scopedCameraDevice->close();

        stopBackgroundThread();
    }

    void onActivityResumed (jobject) override
    {
        // Only care about resumed event when paused event was called first.
        if (! appWasPaused)
            return;

        JUCE_CAMERA_LOG ("appResumed, opening camera...");

        deviceOrientationChangeListener.setEnabled (true);

        startBackgroundThread();

        if (scopedCameraDevice != nullptr)
            scopedCameraDevice->open();
    }

    void startBackgroundThread()
    {
        auto* env = getEnv();

        handlerThread = GlobalRef (LocalRef<jobject> (env->NewObject (AndroidHandlerThread,
                                                                      AndroidHandlerThread.constructor,
                                                                      javaString ("JuceCameraDeviceBackgroundThread").get())));
        // handler thread has to be started before its looper can be fetched
        env->CallVoidMethod (handlerThread, AndroidHandlerThread.start);
        handler = GlobalRef (LocalRef<jobject> (env->NewObject (AndroidHandler,
                                                                AndroidHandler.constructorWithLooper,
                                                                env->CallObjectMethod (handlerThread, AndroidHandlerThread.getLooper))));
    }

    void stopBackgroundThread()
    {
        auto* env = getEnv();

        auto quitSafelyMethod = env->GetMethodID (AndroidHandlerThread, "quitSafely", "()Z");

        // this code will only run on SDK >= 21
        jassert (quitSafelyMethod != nullptr);

        env->CallBooleanMethod (handlerThread, quitSafelyMethod);
        env->CallVoidMethod (handlerThread, AndroidHandlerThread.join);

        jniCheckHasExceptionOccurredAndClear();

        handlerThread.clear();
        handler.clear();
    }

    friend struct CameraDevice::ViewerComponent;

    JUCE_DECLARE_NON_COPYABLE (Pimpl)
};

//==============================================================================
struct CameraDevice::ViewerComponent  : public Component,
                                        private ComponentMovementWatcher
{
    ViewerComponent (CameraDevice& device) : ComponentMovementWatcher (this)
    {
        auto previewSize = device.pimpl->streamConfigurationMap.getDefaultPreviewSize();

        targetAspectRatio = (float) previewSize.getWidth() / (float) previewSize.getHeight();

        if (isOrientationLandscape())
            setBounds (previewSize);
        else
            setBounds (0, 0, previewSize.getHeight(), previewSize.getWidth());

        addAndMakeVisible (viewerComponent);
        viewerComponent.setView (device.pimpl->previewDisplay.getNativeView());
    }

private:
    AndroidViewComponent viewerComponent;

    float targetAspectRatio = 1.0f;

    void componentMovedOrResized (bool, bool) override
    {
        auto b = getLocalBounds().toFloat();

        auto targetWidth  = b.getWidth();
        auto targetHeight = b.getHeight();

        if (isOrientationLandscape())
        {
            auto currentAspectRatio = b.getWidth() / b.getHeight();

            if (currentAspectRatio > targetAspectRatio)
                targetWidth = targetWidth * targetAspectRatio / currentAspectRatio;
            else
                targetHeight = targetHeight * currentAspectRatio / targetAspectRatio;
        }
        else
        {
            auto currentAspectRatio = b.getHeight() / b.getWidth();

            if (currentAspectRatio > targetAspectRatio)
                targetHeight = targetHeight * targetAspectRatio / currentAspectRatio;
            else
                targetWidth = targetWidth * currentAspectRatio / targetAspectRatio;
        }

        viewerComponent.setBounds (Rectangle<float> (targetWidth, targetHeight).withCentre (b.getCentre()).toNearestInt());
    }

    bool isOrientationLandscape() const
    {
        auto o = Desktop::getInstance().getCurrentOrientation();
        return o == Desktop::rotatedClockwise || o == Desktop::rotatedAntiClockwise;
    }

    void componentPeerChanged() override {}
    void componentVisibilityChanged() override {}

    JUCE_DECLARE_NON_COPYABLE (ViewerComponent)
};

String CameraDevice::getFileExtension()
{
    return ".mp4";
}
