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

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

   JUCE is an open source framework subject to commercial or open source
   licensing.

   By downloading, installing, or using the JUCE framework, or combining the
   JUCE framework with any other source code, object code, content or any other
   copyrightable work, you agree to the terms of the JUCE End User Licence
   Agreement, and all incorporated terms including the JUCE Privacy Policy and
   the JUCE Website Terms of Service, as applicable, which will bind you. If you
   do not agree to the terms of these agreements, we will not license the JUCE
   framework to you, and you must discontinue the installation or download
   process and cease use of the JUCE framework.

   JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
   JUCE Privacy Policy: https://juce.com/juce-privacy-policy
   JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/

   Or:

   You may also use this code under the terms of the AGPLv3:
   https://www.gnu.org/licenses/agpl-3.0.en.html

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

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

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

#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK)    \
 METHOD (getPlaybackInfo,      "getPlaybackInfo",      "()Landroid/media/session/MediaController$PlaybackInfo;") \
 METHOD (getPlaybackState,     "getPlaybackState",     "()Landroid/media/session/PlaybackState;") \
 METHOD (getTransportControls, "getTransportControls", "()Landroid/media/session/MediaController$TransportControls;") \
 METHOD (registerCallback,     "registerCallback",     "(Landroid/media/session/MediaController$Callback;)V") \
 METHOD (setVolumeTo,          "setVolumeTo",          "(II)V") \
 METHOD (unregisterCallback,   "unregisterCallback",   "(Landroid/media/session/MediaController$Callback;)V")

DECLARE_JNI_CLASS (AndroidMediaController, "android/media/session/MediaController")
#undef JNI_CLASS_MEMBERS

#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
 METHOD (getAudioAttributes, "getAudioAttributes", "()Landroid/media/AudioAttributes;") \
 METHOD (getCurrentVolume,   "getCurrentVolume",   "()I") \
 METHOD (getMaxVolume,       "getMaxVolume",       "()I")

DECLARE_JNI_CLASS (AndroidMediaControllerPlaybackInfo, "android/media/session/MediaController$PlaybackInfo")
#undef JNI_CLASS_MEMBERS

#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
 METHOD (pause,           "pause",           "()V") \
 METHOD (play,            "play",            "()V") \
 METHOD (playFromMediaId, "playFromMediaId", "(Ljava/lang/String;Landroid/os/Bundle;)V") \
 METHOD (seekTo,          "seekTo",          "(J)V") \
 METHOD (stop,            "stop",            "()V")

DECLARE_JNI_CLASS (AndroidMediaControllerTransportControls, "android/media/session/MediaController$TransportControls")
#undef JNI_CLASS_MEMBERS

#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
 METHOD (constructor,                  "<init>",                       "()V") \
 METHOD (getCurrentPosition,           "getCurrentPosition",           "()I") \
 METHOD (getDuration,                  "getDuration",                  "()I") \
 METHOD (getPlaybackParams,            "getPlaybackParams",            "()Landroid/media/PlaybackParams;") \
 METHOD (getVideoHeight,               "getVideoHeight",               "()I") \
 METHOD (getVideoWidth,                "getVideoWidth",                "()I") \
 METHOD (isPlaying,                    "isPlaying",                    "()Z") \
 METHOD (pause,                        "pause",                        "()V") \
 METHOD (prepareAsync,                 "prepareAsync",                 "()V") \
 METHOD (release,                      "release",                      "()V") \
 METHOD (seekTo,                       "seekTo",                       "(I)V") \
 METHOD (setAudioAttributes,           "setAudioAttributes",           "(Landroid/media/AudioAttributes;)V") \
 METHOD (setDataSource,                "setDataSource",                "(Landroid/content/Context;Landroid/net/Uri;)V") \
 METHOD (setDisplay,                   "setDisplay",                   "(Landroid/view/SurfaceHolder;)V") \
 METHOD (setOnBufferingUpdateListener, "setOnBufferingUpdateListener", "(Landroid/media/MediaPlayer$OnBufferingUpdateListener;)V") \
 METHOD (setOnCompletionListener,      "setOnCompletionListener",      "(Landroid/media/MediaPlayer$OnCompletionListener;)V") \
 METHOD (setOnErrorListener,           "setOnErrorListener",           "(Landroid/media/MediaPlayer$OnErrorListener;)V") \
 METHOD (setOnInfoListener,            "setOnInfoListener",            "(Landroid/media/MediaPlayer$OnInfoListener;)V") \
 METHOD (setOnPreparedListener,        "setOnPreparedListener",        "(Landroid/media/MediaPlayer$OnPreparedListener;)V") \
 METHOD (setOnSeekCompleteListener,    "setOnSeekCompleteListener",    "(Landroid/media/MediaPlayer$OnSeekCompleteListener;)V") \
 METHOD (setPlaybackParams,            "setPlaybackParams",            "(Landroid/media/PlaybackParams;)V") \
 METHOD (setVolume,                    "setVolume",                    "(FF)V") \
 METHOD (start,                        "start",                        "()V") \
 METHOD (stop,                         "stop",                         "()V")

DECLARE_JNI_CLASS (AndroidMediaPlayer, "android/media/MediaPlayer")
#undef JNI_CLASS_MEMBERS

#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
 METHOD (constructor,            "<init>",                 "(Landroid/content/Context;Ljava/lang/String;)V") \
 METHOD (getController,          "getController",          "()Landroid/media/session/MediaController;") \
 METHOD (release,                "release",                "()V") \
 METHOD (setActive,              "setActive",              "(Z)V") \
 METHOD (setCallback,            "setCallback",            "(Landroid/media/session/MediaSession$Callback;)V") \
 METHOD (setFlags,               "setFlags",               "(I)V") \
 METHOD (setMediaButtonReceiver, "setMediaButtonReceiver", "(Landroid/app/PendingIntent;)V") \
 METHOD (setMetadata,            "setMetadata",            "(Landroid/media/MediaMetadata;)V") \
 METHOD (setPlaybackState,       "setPlaybackState",       "(Landroid/media/session/PlaybackState;)V") \
 METHOD (setPlaybackToLocal,     "setPlaybackToLocal",     "(Landroid/media/AudioAttributes;)V")

DECLARE_JNI_CLASS (AndroidMediaSession, "android/media/session/MediaSession")
#undef JNI_CLASS_MEMBERS

#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
 METHOD (build,       "build",   "()Landroid/media/MediaMetadata;") \
 METHOD (constructor, "<init>",  "()V") \
 METHOD (putLong,     "putLong", "(Ljava/lang/String;J)Landroid/media/MediaMetadata$Builder;")

DECLARE_JNI_CLASS (AndroidMediaMetadataBuilder, "android/media/MediaMetadata$Builder")
#undef JNI_CLASS_MEMBERS

#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
 METHOD (getSpeed, "getSpeed", "()F") \
 METHOD (setSpeed, "setSpeed", "(F)Landroid/media/PlaybackParams;")

DECLARE_JNI_CLASS (AndroidPlaybackParams, "android/media/PlaybackParams")
#undef JNI_CLASS_MEMBERS

#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
 METHOD (getActions,       "getActions",       "()J") \
 METHOD (getErrorMessage,  "getErrorMessage",  "()Ljava/lang/CharSequence;") \
 METHOD (getPlaybackSpeed, "getPlaybackSpeed", "()F") \
 METHOD (getPosition,      "getPosition",      "()J") \
 METHOD (getState,         "getState",         "()I")

DECLARE_JNI_CLASS (AndroidPlaybackState, "android/media/session/PlaybackState")
#undef JNI_CLASS_MEMBERS

#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
 METHOD (build,           "build",           "()Landroid/media/session/PlaybackState;") \
 METHOD (constructor,     "<init>",          "()V") \
 METHOD (setActions,      "setActions",      "(J)Landroid/media/session/PlaybackState$Builder;") \
 METHOD (setErrorMessage, "setErrorMessage", "(Ljava/lang/CharSequence;)Landroid/media/session/PlaybackState$Builder;") \
 METHOD (setState,        "setState",        "(IJF)Landroid/media/session/PlaybackState$Builder;")

DECLARE_JNI_CLASS (AndroidPlaybackStateBuilder, "android/media/session/PlaybackState$Builder")
#undef JNI_CLASS_MEMBERS

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

        virtual void onPrepared (LocalRef<jobject>& mediaPlayer) = 0;
        virtual void onBufferingUpdate (LocalRef<jobject>& mediaPlayer, int progress) = 0;
        virtual void onSeekComplete (LocalRef<jobject>& mediaPlayer) = 0;
        virtual void onCompletion (LocalRef<jobject>& mediaPlayer) = 0;
        virtual bool onInfo (LocalRef<jobject>& mediaPlayer, int what, int extra) = 0;
        virtual bool onError (LocalRef<jobject>& mediaPlayer, int what, int extra) = 0;
    };

    MediaPlayerListener (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));

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

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

            owner.onPrepared (mediaPlayer);
            return nullptr;
        }

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

            owner.onCompletion (mediaPlayer);
            return nullptr;
        }

        if (methodName == "onInfo" && numArgs == 3)
        {
            auto mediaPlayer = 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);

            auto res = owner.onInfo (mediaPlayer, whatInt, extraInt);
            return env->CallStaticObjectMethod (JavaBoolean, JavaBoolean.valueOf, (jboolean) res);
        }

        if (methodName == "onError" && numArgs == 3)
        {
            auto mediaPlayer = 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);

            auto res = owner.onError (mediaPlayer, whatInt, extraInt);
            return env->CallStaticObjectMethod (JavaBoolean, JavaBoolean.valueOf, (jboolean) res);
        }

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

            owner.onSeekComplete (mediaPlayer);
            return nullptr;
        }

        if (methodName == "onBufferingUpdate" && numArgs == 2)
        {
            auto mediaPlayer = LocalRef<jobject> (env->GetObjectArrayElement (args, 0));

            auto progress    = LocalRef<jobject> (env->GetObjectArrayElement (args, 1));
            auto progressInt = (int) env->CallIntMethod (progress, JavaInteger.intValue);

            owner.onBufferingUpdate (mediaPlayer, progressInt);

            return nullptr;
        }

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

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

        virtual void onAudioFocusChange (int changeType) = 0;
    };

    AudioManagerOnAudioFocusChangeListener (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));

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

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

            auto changeTypeInt = (int) env->CallIntMethod (changeType, JavaInteger.intValue);

            owner.onAudioFocusChange (changeTypeInt);
            return nullptr;
        }

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

//==============================================================================
struct VideoComponent::Pimpl
    : public AndroidViewComponent, private ActivityLifecycleCallbacks, private SurfaceHolderCallback
{
    Pimpl (VideoComponent& ownerToUse, bool)
        : owner (ownerToUse),
          mediaSession (*this)
       #if JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME
        , systemVolumeListener (*this)
       #endif
    {
        setVisible (true);

        auto* env = getEnv();

        LocalRef<jobject> appContext (getAppContext());

        if (appContext != nullptr)
        {
            ActivityLifecycleCallbacks* callbacks = dynamic_cast<ActivityLifecycleCallbacks*> (this);

            activityLifeListener = GlobalRef (CreateJavaInterface (callbacks, "android/app/Application$ActivityLifecycleCallbacks"));
            env->CallVoidMethod (appContext.get(), AndroidApplication.registerActivityLifecycleCallbacks, activityLifeListener.get());
        }

        {
            LocalRef<jobject> surfaceView (env->NewObject (AndroidSurfaceView, AndroidSurfaceView.constructor, getAppContext().get()));
            LocalRef<jobject> holder (env->CallObjectMethod (surfaceView.get(), AndroidSurfaceView.getHolder));

            SurfaceHolderCallback* callbacks = dynamic_cast<SurfaceHolderCallback*> (this);
            surfaceHolderCallback = GlobalRef (CreateJavaInterface (callbacks, "android/view/SurfaceHolder$Callback"));
            env->CallVoidMethod (holder, AndroidSurfaceHolder.addCallback, surfaceHolderCallback.get());

            setView (surfaceView.get());
        }
    }

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

        if (surfaceHolderCallback != nullptr)
        {
            jobject view = reinterpret_cast<jobject> (getView());

            if (view != nullptr)
            {
                LocalRef <jobject> holder (env->CallObjectMethod (view, AndroidSurfaceView.getHolder));

                env->CallVoidMethod (holder, AndroidSurfaceHolder.removeCallback, surfaceHolderCallback.get());
                SurfaceHolderCallback::clear();
                surfaceHolderCallback.clear();
            }
        }

        if (activityLifeListener != nullptr)
        {
            env->CallVoidMethod (getAppContext().get(), AndroidApplication.unregisterActivityLifecycleCallbacks, activityLifeListener.get());
            ActivityLifecycleCallbacks::clear();
            activityLifeListener.clear();
        }
    }

    void loadAsync (const URL& url, std::function<void (const URL&, Result)> callback)
    {
        close();
        wasOpen = false;

        if (url.isEmpty())
        {
            jassertfalse;
            return;
        }

        if (! url.isLocalFile())
        {
            if (! isPermissionDeclaredInManifest ("android.permission.INTERNET"))
            {
                // In order to access videos from the Internet, the Internet permission has to be specified in
                // Android Manifest.
                jassertfalse;
                return;
            }
        }

        currentURL = url;

        jassert (callback != nullptr);

        loadFinishedCallback = std::move (callback);

        static constexpr jint visible = 0;
        getEnv()->CallVoidMethod ((jobject) getView(), AndroidView.setVisibility, visible);

        mediaSession.load (url);
    }

    void close()
    {
        if (! isOpen())
            return;

        mediaSession.closeVideo();

        static constexpr jint invisible = 4;
        getEnv()->CallVoidMethod ((jobject) getView(), AndroidView.setVisibility, invisible);
    }

    bool isOpen() const noexcept          { return mediaSession.isVideoOpen(); }
    bool isPlaying() const noexcept       { return mediaSession.isPlaying(); }

    void play()                           { mediaSession.play(); }
    void stop()                           { mediaSession.stop(); }

    void setPosition (double newPosition) { mediaSession.setPosition (newPosition); }
    double getPosition() const            { return mediaSession.getPosition(); }

    void setSpeed (double newSpeed)       { mediaSession.setSpeed (newSpeed); }
    double getSpeed() const               { return mediaSession.getSpeed(); }

    Rectangle<int> getNativeSize() const  { return mediaSession.getNativeSize(); }

    double getDuration() const            { return mediaSession.getDuration(); }

    void setVolume (float newVolume)      { mediaSession.setVolume (newVolume); }
    float getVolume() const               { return mediaSession.getVolume(); }

    File currentFile;
    URL currentURL;

private:
    //==============================================================================
    class MediaSession  : private AudioManagerOnAudioFocusChangeListener::Owner
    {
    public:
        MediaSession (Pimpl& ownerToUse)
            : owner (ownerToUse),
              sdkVersion (getAndroidSDKVersion()),
              audioAttributes (getAudioAttributes()),
              nativeMediaSession (LocalRef<jobject> (getEnv()->NewObject (AndroidMediaSession,
                                                                          AndroidMediaSession.constructor,
                                                                          getAppContext().get(),
                                                                          javaString ("JuceVideoMediaSession").get()))),
              mediaSessionCallback (createCallbackObject()),
              playbackStateBuilder (LocalRef<jobject> (getEnv()->NewObject (AndroidPlaybackStateBuilder,
                                                                            AndroidPlaybackStateBuilder.constructor))),
              controller (*this, LocalRef<jobject> (getEnv()->CallObjectMethod (nativeMediaSession,
                                                                                AndroidMediaSession.getController))),
              player (*this),
              audioManager (LocalRef<jobject> (getEnv()->CallObjectMethod (getAppContext().get(), AndroidContext.getSystemService,
                                                                           javaString ("audio").get()))),
              audioFocusChangeListener (*this),
              nativeAudioFocusChangeListener (GlobalRef (CreateJavaInterface (&audioFocusChangeListener,
                                                                              "android/media/AudioManager$OnAudioFocusChangeListener"))),
              audioFocusRequest (createAudioFocusRequestIfNecessary (sdkVersion, audioAttributes,
                                                                     nativeAudioFocusChangeListener))
        {
            auto* env = getEnv();

            env->CallVoidMethod (nativeMediaSession, AndroidMediaSession.setPlaybackToLocal, audioAttributes.get());
            env->CallVoidMethod (nativeMediaSession, AndroidMediaSession.setMediaButtonReceiver, nullptr);
            env->CallVoidMethod (nativeMediaSession, AndroidMediaSession.setCallback, mediaSessionCallback.get());
        }

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

            env->CallVoidMethod (nativeMediaSession, AndroidMediaSession.setCallback, nullptr);

            controller.stop();
            env->CallVoidMethod (nativeMediaSession, AndroidMediaSession.release);
        }

        bool isVideoOpen() const { return player.isVideoOpen(); }
        bool isPlaying() const   { return player.isPlaying(); }

        void load (const URL& url) { controller.load (url); }

        void closeVideo()
        {
            resetState();
            controller.closeVideo();
        }

        void setDisplay (const LocalRef<jobject>& surfaceHolder) { player.setDisplay (surfaceHolder); }

        void play() { controller.play(); }
        void stop() { controller.stop(); }

        void setPosition (double newPosition) { controller.setPosition (newPosition); }
        double getPosition() const            { return controller.getPosition(); }

        void setSpeed (double newSpeed)
        {
            playSpeedMult = newSpeed;

            // Calling non 0.0 speed on a paused player would start it...
            if (player.isPlaying())
            {
                player.setPlaySpeed (playSpeedMult);
                updatePlaybackState();
            }
        }

        double getSpeed() const              { return controller.getPlaySpeed(); }
        Rectangle<int> getNativeSize() const { return player.getVideoNativeSize(); }
        double getDuration() const           { return (double) player.getVideoDuration() / 1000.0; }

        void setVolume (float newVolume)
        {
           #if JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME
            controller.setVolume (newVolume);
           #else
            player.setAudioVolume (newVolume);
           #endif
        }

        float getVolume() const
        {
           #if JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME
            return controller.getVolume();
           #else
            return player.getAudioVolume();
           #endif
        }

        void storeState()
        {
            storedPlaybackState.clear();
            storedPlaybackState = GlobalRef (getCurrentPlaybackState());
        }

        void restoreState()
        {
            if (storedPlaybackState.get() == nullptr)
                return;

            auto* env = getEnv();

            auto pos = env->CallLongMethod (storedPlaybackState, AndroidPlaybackState.getPosition);
            setPosition ((double) pos / 1000.0);

            setSpeed (playSpeedMult);

            auto state = env->CallIntMethod (storedPlaybackState, AndroidPlaybackState.getState);

            if (state != PlaybackState::STATE_NONE && state != PlaybackState::STATE_STOPPED
                && state != PlaybackState::STATE_PAUSED && state != PlaybackState::STATE_ERROR)
            {
                play();
            }
        }

    private:
        struct PlaybackState
        {
            enum
            {
                STATE_NONE = 0,
                STATE_STOPPED = 1,
                STATE_PAUSED = 2,
                STATE_PLAYING = 3,
                STATE_FAST_FORWARDING = 4,
                STATE_REWINDING = 5,
                STATE_BUFFERING = 6,
                STATE_ERROR = 7,
                STATE_CONNECTING = 8,
                STATE_SKIPPING_TO_PREVIOUS = 9,
                STATE_SKIPPING_TO_NEXT = 10,
                STATE_SKIPPING_TO_QUEUE_ITEM = 11,
            };

            enum
            {
                ACTION_PAUSE              = 0x2,
                ACTION_PLAY               = 0x4,
                ACTION_PLAY_FROM_MEDIA_ID = 0x8000,
                ACTION_PLAY_PAUSE         = 0x200,
                ACTION_SEEK_TO            = 0x100,
                ACTION_STOP               = 0x1,
            };
        };

        //==============================================================================
        class Controller
        {
        public:
            Controller (MediaSession& ownerToUse, const LocalRef<jobject>& nativeControllerToUse)
                : owner (ownerToUse),
                  nativeController (GlobalRef (nativeControllerToUse)),
                  controllerTransportControls (LocalRef<jobject> (getEnv()->CallObjectMethod (nativeControllerToUse,
                                                                                              AndroidMediaController.getTransportControls))),
                  controllerCallback (createControllerCallbacks())
            {
                auto* env = getEnv();

                env->CallVoidMethod (nativeController, AndroidMediaController.registerCallback, controllerCallback.get());
            }

            ~Controller()
            {
                auto* env = getEnv();
                env->CallVoidMethod (nativeController, AndroidMediaController.unregisterCallback, controllerCallback.get());
            }

            void load (const URL& url)
            {
                // NB: would use playFromUri, but it was only introduced in API 23...
                getEnv()->CallVoidMethod (controllerTransportControls, AndroidMediaControllerTransportControls.playFromMediaId,
                                          javaString (url.toString (true)).get(), nullptr);
            }

            void closeVideo()
            {
                getEnv()->CallVoidMethod (controllerTransportControls, AndroidMediaControllerTransportControls.stop);
            }

            void play()
            {
                getEnv()->CallVoidMethod (controllerTransportControls, AndroidMediaControllerTransportControls.play);
            }

            void stop()
            {
                // NB: calling pause, rather than stop, because after calling stop, we would have to call load() again.
                getEnv()->CallVoidMethod (controllerTransportControls, AndroidMediaControllerTransportControls.pause);
            }

            void setPosition (double newPosition)
            {
                auto seekPos = static_cast<jlong> (newPosition * 1000);

                getEnv()->CallVoidMethod (controllerTransportControls, AndroidMediaControllerTransportControls.seekTo, seekPos);
            }

            double getPosition() const
            {
                auto* env = getEnv();

                auto playbackState = LocalRef<jobject> (env->CallObjectMethod (nativeController, AndroidMediaController.getPlaybackState));

                if (playbackState != nullptr)
                    return (double) env->CallLongMethod (playbackState, AndroidPlaybackState.getPosition) / 1000.0;

                return 0.0;
            }

            double getPlaySpeed() const
            {
                auto* env = getEnv();

                auto playbackState = LocalRef<jobject> (env->CallObjectMethod (nativeController, AndroidMediaController.getPlaybackState));

                if (playbackState != nullptr)
                    return (double) env->CallFloatMethod (playbackState, AndroidPlaybackState.getPlaybackSpeed);

                return 1.0;
            }

            void setVolume (float newVolume)
            {
                auto* env = getEnv();

                auto playbackInfo = LocalRef<jobject> (env->CallObjectMethod (nativeController, AndroidMediaController.getPlaybackInfo));

                auto maxVolume = env->CallIntMethod (playbackInfo, AndroidMediaControllerPlaybackInfo.getMaxVolume);

                auto targetVolume = jmin (jint ((float) maxVolume * newVolume), maxVolume);

                static constexpr jint flagShowUI = 1;
                env->CallVoidMethod (nativeController, AndroidMediaController.setVolumeTo, targetVolume, flagShowUI);
            }

            float getVolume() const
            {
                auto* env = getEnv();

                auto playbackInfo = LocalRef<jobject> (env->CallObjectMethod (nativeController, AndroidMediaController.getPlaybackInfo));

                auto maxVolume = (int) (env->CallIntMethod (playbackInfo, AndroidMediaControllerPlaybackInfo.getMaxVolume));
                auto curVolume = (int) (env->CallIntMethod (playbackInfo, AndroidMediaControllerPlaybackInfo.getCurrentVolume));

                return static_cast<float> (curVolume) / (float) maxVolume;
            }

        private:
            MediaSession& owner;

            GlobalRef nativeController;
            GlobalRef controllerTransportControls;
            GlobalRef controllerCallback;
            bool wasPlaying = false;
            bool wasPaused = true;

            //==============================================================================
            void stateChanged (jobject playbackState)
            {
                JUCE_VIDEO_LOG ("MediaSessionController::playbackStateChanged()");

                if (playbackState == nullptr)
                    return;

                auto state = getEnv()->CallIntMethod (playbackState, AndroidPlaybackState.getState);

                static constexpr jint statePaused  = 2;
                static constexpr jint statePlaying = 3;

                if (wasPlaying == false && state == statePlaying)
                    owner.playbackStarted();
                else if (wasPaused == false && state == statePaused)
                    owner.playbackStopped();

                wasPlaying = state == statePlaying;
                wasPaused  = state == statePaused;
            }

            //==============================================================================
            #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
                METHOD (constructor, "<init>", "(J)V")                  \
                CALLBACK (generatedCallback<&Controller::audioInfoChanged>,     "mediaControllerAudioInfoChanged",      "(JLandroid/media/session/MediaController$PlaybackInfo;)V") \
                CALLBACK (generatedCallback<&Controller::metadataChanged>,      "mediaControllerMetadataChanged",       "(JLandroid/media/MediaMetadata;)V") \
                CALLBACK (generatedCallback<&Controller::playbackStateChanged>, "mediaControllerPlaybackStateChanged",  "(JLandroid/media/session/PlaybackState;)V") \
                CALLBACK (generatedCallback<&Controller::sessionDestroyed>,     "mediaControllerSessionDestroyed",      "(J)V")

            DECLARE_JNI_CLASS_WITH_BYTECODE (AndroidMediaControllerCallback, "com/rmsl/juce/MediaControllerCallback", 21, MediaSessionByteCode)
           #undef JNI_CLASS_MEMBERS

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

            //==============================================================================
            // MediaSessionController callbacks
            static void audioInfoChanged (JNIEnv*, [[maybe_unused]] Controller& t, [[maybe_unused]] jobject playbackInfo)
            {
                JUCE_VIDEO_LOG ("MediaSessionController::audioInfoChanged()");
            }

            static void metadataChanged (JNIEnv*, [[maybe_unused]] Controller&, [[maybe_unused]] jobject metadata)
            {
                JUCE_VIDEO_LOG ("MediaSessionController::metadataChanged()");
            }

            static void playbackStateChanged (JNIEnv*, Controller& t, [[maybe_unused]] jobject state)
            {
                t.stateChanged (state);
            }

            static void sessionDestroyed (JNIEnv*, [[maybe_unused]] Controller& t)
            {
                JUCE_VIDEO_LOG ("MediaSessionController::sessionDestroyed()");
            }
        };

        //==============================================================================
        class Player   : private MediaPlayerListener::Owner
        {
        public:
            Player (MediaSession& ownerToUse)
                : owner (ownerToUse),
                  mediaPlayerListener (*this),
                  nativeMediaPlayerListener (GlobalRef (CreateJavaInterface (&mediaPlayerListener,
                                                                             getNativeMediaPlayerListenerInterfaces())))

            {}

            void setDisplay (const LocalRef<jobject>& surfaceHolder)
            {
                if (surfaceHolder == nullptr)
                {
                    videoSurfaceHolder.clear();

                    if (nativeMediaPlayer.get() != nullptr)
                        getEnv()->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.setDisplay, nullptr);

                    return;
                }

                videoSurfaceHolder = GlobalRef (surfaceHolder);

                if (nativeMediaPlayer.get() != nullptr)
                    getEnv()->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.setDisplay, videoSurfaceHolder.get());
            }

            void load (const LocalRef<jstring>& mediaId, [[maybe_unused]] const LocalRef<jobject>& extras)
            {
                closeVideo();

                auto* env = getEnv();

                nativeMediaPlayer = GlobalRef (LocalRef<jobject> (env->NewObject (AndroidMediaPlayer, AndroidMediaPlayer.constructor)));

                currentState = State::idle;

                auto uri = LocalRef<jobject> (env->CallStaticObjectMethod (AndroidUri, AndroidUri.parse, mediaId.get()));
                env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.setDataSource, getAppContext().get(), uri.get());

                if (jniCheckHasExceptionOccurredAndClear())
                {
                    owner.errorOccurred ("Could not find video under path provided (" + juceString (mediaId) + ")");
                    return;
                }

                currentState = State::initialised;

                env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.setOnBufferingUpdateListener,   nativeMediaPlayerListener.get());
                env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.setOnCompletionListener, nativeMediaPlayerListener.get());
                env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.setOnErrorListener,      nativeMediaPlayerListener.get());
                env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.setOnInfoListener,       nativeMediaPlayerListener.get());
                env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.setOnPreparedListener,   nativeMediaPlayerListener.get());
                env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.setOnSeekCompleteListener,   nativeMediaPlayerListener.get());

                if (videoSurfaceHolder != nullptr)
                    env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.setDisplay, videoSurfaceHolder.get());

                env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.prepareAsync);

                currentState = State::preparing;
            }

            void closeVideo()
            {
                if (nativeMediaPlayer.get() == nullptr)
                    return;

                auto* env = getEnv();

                if (getCurrentStateInfo().canCallStop)
                    env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.stop);

                env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.release);
                nativeMediaPlayer.clear();

                currentState = State::end;
            }

            bool isVideoOpen() const noexcept
            {
                return currentState == State::prepared || currentState == State::started
                    || currentState == State::paused || currentState == State::complete;
            }

            int getPlaybackStateFlag() const noexcept { return getCurrentStateInfo().playbackStateFlag; }
            int getAllowedActions()    const noexcept { return getCurrentStateInfo().allowedActions; }

            jlong getVideoDuration() const
            {
                if (! getCurrentStateInfo().canCallGetVideoDuration)
                    return 0;

                return getEnv()->CallIntMethod (nativeMediaPlayer, AndroidMediaPlayer.getDuration);
            }

            Rectangle<int> getVideoNativeSize() const
            {
                if (! getCurrentStateInfo().canCallGetVideoHeight)
                {
                    jassertfalse;
                    return {};
                }

                auto* env = getEnv();

                auto width  = (int) env->CallIntMethod (nativeMediaPlayer, AndroidMediaPlayer.getVideoWidth);
                auto height = (int) env->CallIntMethod (nativeMediaPlayer, AndroidMediaPlayer.getVideoHeight);

                return Rectangle<int> (0, 0, width, height);
            }

            void play()
            {
                if (! getCurrentStateInfo().canCallStart)
                {
                    jassertfalse;
                    return;
                }

                auto* env = getEnv();

                // Perform a potentially pending volume setting
                if (! exactlyEqual (lastAudioVolume, std::numeric_limits<float>::min()))
                    env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.setVolume, (jfloat) lastAudioVolume, (jfloat) lastAudioVolume);

                env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.start);

                currentState = State::started;
            }

            void pause()
            {
                if (! getCurrentStateInfo().canCallPause)
                {
                    jassertfalse;
                    return;
                }

                getEnv()->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.pause);

                currentState = State::paused;
            }

            bool isPlaying() const
            {
                return getCurrentStateInfo().isPlaying;
            }

            void setPlayPosition (jint newPositionMs)
            {
                if (! getCurrentStateInfo().canCallSeekTo)
                {
                    jassertfalse;
                    return;
                }

                getEnv()->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.seekTo, (jint) newPositionMs);
            }

            jint getPlayPosition() const
            {
                if (! getCurrentStateInfo().canCallGetCurrentPosition)
                    return 0.0;

                return getEnv()->CallIntMethod (nativeMediaPlayer, AndroidMediaPlayer.getCurrentPosition);
            }

            void setPlaySpeed (double newSpeed)
            {
                if (! getCurrentStateInfo().canCallSetPlaybackParams)
                {
                    jassertfalse;
                    return;
                }

                auto* env = getEnv();

                auto playbackParams = LocalRef<jobject> (env->CallObjectMethod (nativeMediaPlayer, AndroidMediaPlayer.getPlaybackParams));
                LocalRef<jobject> (env->CallObjectMethod (playbackParams, AndroidPlaybackParams.setSpeed, (jfloat) newSpeed));
                env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.setPlaybackParams, playbackParams.get());

                if (jniCheckHasExceptionOccurredAndClear())
                {
                    // MediaPlayer can't handle speed provided!
                    jassertfalse;
                }
            }

            double getPlaySpeed() const
            {
                if (! getCurrentStateInfo().canCallGetPlaybackParams)
                    return 0.0;

                auto* env = getEnv();

                auto playbackParams = LocalRef<jobject> (env->CallObjectMethod (nativeMediaPlayer, AndroidMediaPlayer.getPlaybackParams));
                return (double) env->CallFloatMethod (playbackParams, AndroidPlaybackParams.getSpeed);
            }

            void setAudioVolume (float newVolume)
            {
                if (! getCurrentStateInfo().canCallSetVolume)
                {
                    jassertfalse;
                    return;
                }

                lastAudioVolume = jlimit (0.0f, 1.0f, newVolume);

                if (nativeMediaPlayer.get() != nullptr)
                    getEnv()->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.setVolume, (jfloat) lastAudioVolume, (jfloat) lastAudioVolume);
            }

            float getAudioVolume() const
            {
                // There is NO getVolume() in MediaPlayer, so the value returned here can be incorrect!
                return lastAudioVolume;
            }

        private:
            //==============================================================================
            struct StateInfo
            {
                int playbackStateFlag = 0, allowedActions = 0;

                bool isPlaying, canCallGetCurrentPosition, canCallGetVideoDuration,
                        canCallGetVideoHeight, canCallGetVideoWidth, canCallGetPlaybackParams,
                        canCallPause, canCallPrepare, canCallSeekTo, canCallSetAudioAttributes,
                        canCallSetDataSource, canCallSetPlaybackParams, canCallSetVolume,
                        canCallStart, canCallStop;
            };

            enum class State
            {
                idle, initialised, preparing, prepared, started, paused, stopped, complete, error, end
            };

            static constexpr StateInfo stateInfos[] = {
                /* idle */
                {PlaybackState::STATE_NONE, PlaybackState::ACTION_PLAY_FROM_MEDIA_ID,
                            false, true, false, true, true, false, false, false, false, true,
                            true,  false, true, false, false},
                /* initialised */
                {PlaybackState::STATE_NONE, 0, // NB: could use action prepare, but that's API 24 onwards only
                            false, true, false, true, true, true, false, true, false, true,
                            false,  true, true, false, false},
                /* preparing */
                {PlaybackState::STATE_BUFFERING, 0,
                            false, false, false, false, false, true, false, false, false, false,
                            false,  false, false, false, false},
                /* prepared */
                {PlaybackState::STATE_PAUSED,
                            PlaybackState::ACTION_PLAY | PlaybackState::ACTION_PLAY_PAUSE | PlaybackState::ACTION_PLAY_FROM_MEDIA_ID | PlaybackState::ACTION_STOP | PlaybackState::ACTION_SEEK_TO,
                            false, true, true, true, true, true, false, false, true, true,
                            false, true, true, true, true},
                /* started */
                {PlaybackState::STATE_PLAYING,
                            PlaybackState::ACTION_PAUSE | PlaybackState::ACTION_PLAY_PAUSE | PlaybackState::ACTION_SEEK_TO | PlaybackState::ACTION_STOP | PlaybackState::ACTION_PLAY_FROM_MEDIA_ID,
                            true, true, true, true, true, true, true, false, true, true,
                            false, true, true, true, true},
                /* paused */
                {PlaybackState::STATE_PAUSED,
                            PlaybackState::ACTION_PLAY | PlaybackState::ACTION_PLAY_PAUSE | PlaybackState::ACTION_SEEK_TO | PlaybackState::ACTION_STOP | PlaybackState::ACTION_PLAY_FROM_MEDIA_ID,
                            false, true, true, true, true, true, true, false, true, true,
                            false, true, true, true, true},
                /* stopped */
                {PlaybackState::STATE_STOPPED,
                            PlaybackState::ACTION_PLAY_FROM_MEDIA_ID,
                            false, true, true, true, true, true, false, true, false, true,
                            false, false, true, false, true},
                /* complete */
                {PlaybackState::STATE_PAUSED,
                            PlaybackState::ACTION_SEEK_TO | PlaybackState::ACTION_STOP | PlaybackState::ACTION_PLAY_FROM_MEDIA_ID,
                            false, true, true, true, true, true, true, false, true, true,
                            false, true, true, true, true},
                /* error */
                {PlaybackState::STATE_ERROR,
                            PlaybackState::ACTION_PLAY_FROM_MEDIA_ID,
                            false, false, false, false, false, false, false, false, false, false,
                            false, false, false, false, false},
                /* end */
                {PlaybackState::STATE_NONE,
                            PlaybackState::ACTION_PLAY_FROM_MEDIA_ID,
                            false, false, false, false, false, false, false, false, false, false,
                            false, false, false, false, false}
            };

            StateInfo getCurrentStateInfo() const noexcept             { return stateInfos[static_cast<int> (currentState)]; }

            //==============================================================================
            MediaSession& owner;
            GlobalRef nativeMediaPlayer;

            MediaPlayerListener mediaPlayerListener;
            GlobalRef nativeMediaPlayerListener;

            float lastAudioVolume = std::numeric_limits<float>::min();

            GlobalRef videoSurfaceHolder;

            State currentState = State::idle;

            //==============================================================================
            void onPrepared ([[maybe_unused]] LocalRef<jobject>& mediaPlayer) override
            {
                JUCE_VIDEO_LOG ("MediaPlayer::onPrepared()");

                currentState = State::prepared;

                owner.playerPrepared();
            }

            void onBufferingUpdate ([[maybe_unused]] LocalRef<jobject>& mediaPlayer, int progress) override
            {
                owner.playerBufferingUpdated (progress);
            }

            void onSeekComplete ([[maybe_unused]] LocalRef<jobject>& mediaPlayer) override
            {
                JUCE_VIDEO_LOG ("MediaPlayer::onSeekComplete()");

                owner.playerSeekCompleted();
            }

            void onCompletion ([[maybe_unused]] LocalRef<jobject>& mediaPlayer) override
            {
                JUCE_VIDEO_LOG ("MediaPlayer::onCompletion()");

                currentState = State::complete;

                owner.playerPlaybackCompleted();
            }

            enum
            {
                MEDIA_INFO_UNKNOWN               = 1,
                MEDIA_INFO_VIDEO_RENDERING_START = 3,
                MEDIA_INFO_VIDEO_TRACK_LAGGING   = 700,
                MEDIA_INFO_BUFFERING_START       = 701,
                MEDIA_INFO_BUFFERING_END         = 702,
                MEDIA_INFO_NETWORK_BANDWIDTH     = 703,
                MEDIA_INFO_BAD_INTERLEAVING      = 800,
                MEDIA_INFO_NOT_SEEKABLE          = 801,
                MEDIA_INFO_METADATA_UPDATE       = 802,
                MEDIA_INFO_AUDIO_NOT_PLAYING     = 804,
                MEDIA_INFO_VIDEO_NOT_PLAYING     = 805,
                MEDIA_INFO_UNSUPPORTED_SUBTITE   = 901,
                MEDIA_INFO_SUBTITLE_TIMED_OUT    = 902
            };

            bool onInfo ([[maybe_unused]] LocalRef<jobject>& mediaPlayer, int what, [[maybe_unused]] int extra) override
            {
                JUCE_VIDEO_LOG ("MediaPlayer::onInfo(), infoCode: " + String (what) + " (" + infoCodeToString (what) + ")"
                                + ", extraCode: " + String (extra));

                if (what == MEDIA_INFO_BUFFERING_START)
                    owner.playerBufferingStarted();
                else if (what == MEDIA_INFO_BUFFERING_END)
                    owner.playerBufferingEnded();

                return true;
            }

            static String infoCodeToString (int code)
            {
                switch (code)
                {
                    case MEDIA_INFO_UNKNOWN:               return "Unknown";
                    case MEDIA_INFO_VIDEO_RENDERING_START: return "Rendering start";
                    case MEDIA_INFO_VIDEO_TRACK_LAGGING:   return "Video track lagging";
                    case MEDIA_INFO_BUFFERING_START:       return "Buffering start";
                    case MEDIA_INFO_BUFFERING_END:         return "Buffering end";
                    case MEDIA_INFO_NETWORK_BANDWIDTH:     return "Network bandwidth info available";
                    case MEDIA_INFO_BAD_INTERLEAVING:      return "Bad interleaving";
                    case MEDIA_INFO_NOT_SEEKABLE:          return "Video not seekable";
                    case MEDIA_INFO_METADATA_UPDATE:       return "Metadata updated";
                    case MEDIA_INFO_AUDIO_NOT_PLAYING:     return "Audio not playing";
                    case MEDIA_INFO_VIDEO_NOT_PLAYING:     return "Video not playing";
                    case MEDIA_INFO_UNSUPPORTED_SUBTITE:   return "Unsupported subtitle";
                    case MEDIA_INFO_SUBTITLE_TIMED_OUT:    return "Subtitle timed out";
                    default:                               return "";
                }
            }

            bool onError ([[maybe_unused]] LocalRef<jobject>& mediaPlayer, int what, int extra) override
            {
                auto errorMessage = errorCodeToString (what);
                auto extraMessage = errorCodeToString (extra);

                if (extraMessage.isNotEmpty())
                    errorMessage << ", " << extraMessage;

                JUCE_VIDEO_LOG ("MediaPlayer::onError(), errorCode: " + String (what) + " (" + errorMessage + ")"
                                + ", extraCode: " + String (extra) + " (" + extraMessage + ")");

                currentState = State::error;

                owner.errorOccurred (errorMessage);
                return true;
            }

            static String errorCodeToString (int code)
            {
                enum
                {
                    MEDIA_ERROR_UNSUPPORTED                        = -1010,
                    MEDIA_ERROR_MALFORMED                          = -1007,
                    MEDIA_ERROR_IO                                 = -1004,
                    MEDIA_ERROR_TIMED_OUT                          = -110,
                    MEDIA_ERROR_UNKNOWN                            = 1,
                    MEDIA_ERROR_SERVER_DIED                        = 100,
                    MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK = 200
                };

                switch (code)
                {
                    case MEDIA_ERROR_UNSUPPORTED:                        return "Unsupported bitstream";
                    case MEDIA_ERROR_MALFORMED:                          return "Malformed bitstream";
                    case MEDIA_ERROR_IO:                                 return "File/Network I/O error";
                    case MEDIA_ERROR_TIMED_OUT:                          return "Timed out";
                    case MEDIA_ERROR_UNKNOWN:                            return "Unknown error";
                    case MEDIA_ERROR_SERVER_DIED:                        return "Media server died (playback restart required)";
                    case MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK: return "Video container not valid for progressive playback";
                    default:                                             return "";
                }
            }

            //==============================================================================
            static StringArray getNativeMediaPlayerListenerInterfaces()
            {
                #define IFPREFIX "android/media/MediaPlayer$"

                return { IFPREFIX "OnCompletionListener", IFPREFIX "OnErrorListener",
                         IFPREFIX "OnInfoListener", IFPREFIX "OnPreparedListener",
                         IFPREFIX "OnBufferingUpdateListener", IFPREFIX "OnSeekCompleteListener"
                };

                #undef IFPREFIX
            }
        };

        //==============================================================================
        Pimpl& owner;
        int sdkVersion;

        GlobalRef audioAttributes;
        GlobalRef nativeMediaSession;
        GlobalRef mediaSessionCallback;
        GlobalRef playbackStateBuilder;

        Controller controller;
        Player player;

        GlobalRef audioManager;
        AudioManagerOnAudioFocusChangeListener audioFocusChangeListener;
        GlobalRef nativeAudioFocusChangeListener;
        GlobalRef audioFocusRequest;

        GlobalRef storedPlaybackState;

        bool pendingSeekRequest = false;

        bool playerBufferingInProgress = false;
        bool usesBuffering = false;
        SparseSet<int> bufferedRegions;

        double playSpeedMult = 1.0;
        bool hasAudioFocus = false;

        //==============================================================================
        #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
            METHOD (constructor, "<init>", "(J)V")                      \
            CALLBACK (generatedCallback<&MediaSession::pauseCallback>,           "mediaSessionPause",           "(J)V") \
            CALLBACK (generatedCallback<&MediaSession::playCallback>,            "mediaSessionPlay",            "(J)V") \
            CALLBACK (generatedCallback<&MediaSession::playFromMediaIdCallback>, "mediaSessionPlayFromMediaId", "(JLjava/lang/String;Landroid/os/Bundle;)V") \
            CALLBACK (generatedCallback<&MediaSession::seekToCallback>,          "mediaSessionSeekTo",          "(JJ)V") \
            CALLBACK (generatedCallback<&MediaSession::stopCallback>,            "mediaSessionStop",            "(J)V")

        DECLARE_JNI_CLASS (AndroidMediaSessionCallback, "com/rmsl/juce/MediaSessionCallback")
        #undef JNI_CLASS_MEMBERS

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

        //==============================================================================
        // MediaSession callbacks
        static void pauseCallback (JNIEnv*, MediaSession& t)
        {
            JUCE_VIDEO_LOG ("MediaSession::pauseCallback()");
            t.player.pause();
            t.updatePlaybackState();
            t.abandonAudioFocus();
        }

        static void playCallback (JNIEnv* env, MediaSession& t)
        {
            JUCE_VIDEO_LOG ("MediaSession::playCallback()");

            t.requestAudioFocus();

            if (! t.hasAudioFocus)
            {
                t.errorOccurred ("Application has been denied audio focus. Try again later.");
                return;
            }

            env->CallVoidMethod (t.nativeMediaSession, AndroidMediaSession.setActive, true);

            t.player.play();
            t.setSpeed (t.playSpeedMult);
            t.updatePlaybackState();
        }

        static void playFromMediaIdCallback (JNIEnv* env, MediaSession& t, jstring mediaId, jobject extras)
        {
            JUCE_VIDEO_LOG ("MediaSession::playFromMediaIdCallback()");

            t.player.load (LocalRef<jstring> ((jstring) env->NewLocalRef (mediaId)), LocalRef<jobject> (env->NewLocalRef (extras)));
            t.updatePlaybackState();
        }

        static void seekToCallback (JNIEnv*, MediaSession& t, jlong pos)
        {
            JUCE_VIDEO_LOG ("MediaSession::seekToCallback()");

            t.pendingSeekRequest = true;
            t.player.setPlayPosition ((jint) pos);
            t.updatePlaybackState();
        }

        static void stopCallback (JNIEnv* env, MediaSession& t)
        {
            JUCE_VIDEO_LOG ("MediaSession::stopCallback()");

            env->CallVoidMethod (t.nativeMediaSession, AndroidMediaSession.setActive, false);

            t.player.closeVideo();
            t.updatePlaybackState();

            t.abandonAudioFocus();

            t.owner.closeVideoFinished();
        }

        //==============================================================================
        bool isSeekInProgress() const noexcept
        {
            if (pendingSeekRequest)
                return true;

            if (! usesBuffering)
                return false;

            // NB: player sometimes notifies us about buffering, but only for regions that
            // were previously buffered already. For buffering happening for the first time,
            // we don't get such notification...
            if (playerBufferingInProgress)
                return true;

            auto playPos = player.getPlayPosition();
            auto durationMs = player.getVideoDuration();
            auto playPosPercent = (int) (100.0 * playPos / static_cast<double> (durationMs));

            // NB: assuming the playback will start roughly when there is 5% of content loaded...
            return ! bufferedRegions.containsRange (Range<int> (playPosPercent, jmin (101, playPosPercent + 5)));
        }

        void updatePlaybackState()
        {
            getEnv()->CallVoidMethod (nativeMediaSession, AndroidMediaSession.setPlaybackState, getCurrentPlaybackState().get());
        }

        LocalRef<jobject> getCurrentPlaybackState()
        {
            static constexpr int bufferingState = 6;

            auto playbackStateFlag = isSeekInProgress() ? bufferingState : player.getPlaybackStateFlag();
            auto playPos = player.getPlayPosition();
            auto playSpeed = player.getPlaySpeed();
            auto allowedActions = player.getAllowedActions();

            auto* env = getEnv();

            LocalRef<jobject> (env->CallObjectMethod (playbackStateBuilder, AndroidPlaybackStateBuilder.setState,
                                                      (jint) playbackStateFlag, (jlong) playPos, (jfloat) playSpeed));

            LocalRef<jobject> (env->CallObjectMethod (playbackStateBuilder, AndroidPlaybackStateBuilder.setActions, (jint) allowedActions));

            return LocalRef<jobject> (env->CallObjectMethod (playbackStateBuilder, AndroidPlaybackStateBuilder.build));
        }

        //==============================================================================
        void playerPrepared()
        {
            resetState();

            updateMetadata();

            owner.loadFinished();
        }

        void playerBufferingStarted() { playerBufferingInProgress = true; }
        void playerBufferingEnded()   { playerBufferingInProgress = false; }

        void playerBufferingUpdated (int progress)
        {
            usesBuffering = true;

            updatePlaybackState();

            auto playPos = player.getPlayPosition();
            auto durationMs = player.getVideoDuration();
            auto playPosPercent = (int) (100.0 * playPos / static_cast<double> (durationMs));

            bufferedRegions.addRange (Range<int> (playPosPercent, progress + 1));

            String ranges;

            for (auto& r : bufferedRegions.getRanges())
                ranges << "[" << r.getStart() << "%, " << r.getEnd() - 1 << "%] ";

            JUCE_VIDEO_LOG ("Buffering status update, seek pos: " + String (playPosPercent) + "%, buffered regions: " + ranges);
        }

        void playerSeekCompleted()
        {
            pendingSeekRequest = false;
            updatePlaybackState();
        }

        void playerPlaybackCompleted()
        {
            player.pause();
            abandonAudioFocus();

            pendingSeekRequest = true;
            player.setPlayPosition (0);
            updatePlaybackState();
        }

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

            auto metadataBuilder = LocalRef<jobject> (env->NewObject (AndroidMediaMetadataBuilder,
                                                                      AndroidMediaMetadataBuilder.constructor));

            auto durationMs = player.getVideoDuration();

            auto jDurationKey = javaString ("android.media.metadata.DURATION");
            LocalRef<jobject> (env->CallObjectMethod (metadataBuilder,
                                                      AndroidMediaMetadataBuilder.putLong,
                                                      jDurationKey.get(),
                                                      (jlong) durationMs));

            auto jNumTracksKey = javaString ("android.media.metadata.NUM_TRACKS");
            LocalRef<jobject> (env->CallObjectMethod (metadataBuilder,
                                                      AndroidMediaMetadataBuilder.putLong,
                                                      jNumTracksKey.get(),
                                                      (jlong) 1));

            env->CallVoidMethod (nativeMediaSession, AndroidMediaSession.setMetadata,
                                 env->CallObjectMethod (metadataBuilder, AndroidMediaMetadataBuilder.build));
        }

        void errorOccurred (const String& errorMessage)
        {
            auto* env = getEnv();

            // Propagate error to session controller(s) and ...
            LocalRef<jobject> (env->CallObjectMethod (playbackStateBuilder, AndroidPlaybackStateBuilder.setErrorMessage,
                                                      javaString (errorMessage).get()));

            auto state = LocalRef<jobject> (env->CallObjectMethod (playbackStateBuilder, AndroidPlaybackStateBuilder.build));
            env->CallVoidMethod (nativeMediaSession, AndroidMediaSession.setPlaybackState, state.get());

            // ...also notify JUCE side client
            owner.errorOccurred (errorMessage);
        }

        //==============================================================================
        static LocalRef<jobject> createAudioFocusRequestIfNecessary (int sdkVersion, const GlobalRef& audioAttributes,
                                                                     const GlobalRef& nativeAudioFocusChangeListener)
        {
            if (sdkVersion < 26)
                return LocalRef<jobject>();

            auto* env = getEnv();

            auto requestBuilderClass = LocalRef<jclass> (env->FindClass ("android/media/AudioFocusRequest$Builder"));

            static jmethodID constructor = env->GetMethodID (requestBuilderClass, "<init>", "(I)V");
            static jmethodID buildMethod = env->GetMethodID (requestBuilderClass, "build", "()Landroid/media/AudioFocusRequest;");
            static jmethodID setAudioAttributesMethod = env->GetMethodID (requestBuilderClass, "setAudioAttributes",
                                                                          "(Landroid/media/AudioAttributes;)Landroid/media/AudioFocusRequest$Builder;");
            static jmethodID setOnAudioFocusChangeListenerMethod = env->GetMethodID (requestBuilderClass, "setOnAudioFocusChangeListener",
                                                                                     "(Landroid/media/AudioManager$OnAudioFocusChangeListener;)Landroid/media/AudioFocusRequest$Builder;");

            static constexpr jint audioFocusGain = 1;

            auto requestBuilder = LocalRef<jobject> (env->NewObject (requestBuilderClass, constructor, audioFocusGain));
            LocalRef<jobject> (env->CallObjectMethod (requestBuilder, setAudioAttributesMethod, audioAttributes.get()));
            LocalRef<jobject> (env->CallObjectMethod (requestBuilder, setOnAudioFocusChangeListenerMethod, nativeAudioFocusChangeListener.get()));

            return LocalRef<jobject> (env->CallObjectMethod (requestBuilder, buildMethod));
        }

        void requestAudioFocus()
        {
            static constexpr jint audioFocusGain = 1;
            static constexpr jint streamMusic = 3;
            static constexpr jint audioFocusRequestGranted = 1;

            jint result = audioFocusRequestGranted;

            if (sdkVersion >= 26)
            {
                static jmethodID requestAudioFocusMethod = getEnv()->GetMethodID (AndroidAudioManager, "requestAudioFocus",
                                                                                  "(Landroid/media/AudioFocusRequest;)I");

                result = getEnv()->CallIntMethod (audioManager, requestAudioFocusMethod, audioFocusRequest.get());
            }
            else
            {
                result = getEnv()->CallIntMethod (audioManager, AndroidAudioManager.requestAudioFocus,
                                                  nativeAudioFocusChangeListener.get(), streamMusic, audioFocusGain);
            }

            hasAudioFocus = result == audioFocusRequestGranted;
        }

        void abandonAudioFocus()
        {
            if (! hasAudioFocus)
                return;

            static constexpr jint audioFocusRequestGranted = 1;

            jint result = audioFocusRequestGranted;

            if (sdkVersion >= 26)
            {
                static jmethodID abandonAudioFocusMethod = getEnv()->GetMethodID (AndroidAudioManager, "abandonAudioFocusRequest",
                                                                                  "(Landroid/media/AudioFocusRequest;)I");

                result = getEnv()->CallIntMethod (audioManager, abandonAudioFocusMethod, audioFocusRequest.get());
            }
            else
            {
                result = getEnv()->CallIntMethod (audioManager, AndroidAudioManager.abandonAudioFocus,
                                                   nativeAudioFocusChangeListener.get());
            }

            // NB: granted in this case means "granted to change the focus to abandoned"...
            hasAudioFocus = result != audioFocusRequestGranted;
        }

        void onAudioFocusChange (int changeType) override
        {
            static constexpr jint audioFocusGain = 1;

            if (changeType == audioFocusGain)
                JUCE_VIDEO_LOG ("Audio focus gained");
            else
                JUCE_VIDEO_LOG ("Audio focus lost");

            if (changeType != audioFocusGain)
            {
                if (isPlaying())
                {
                    JUCE_VIDEO_LOG ("Received a request to abandon audio focus. Stopping playback...");
                    stop();
                }

                abandonAudioFocus();
            }
        }

        //==============================================================================
        void playbackStarted()
        {
            owner.playbackStarted();
        }

        void playbackStopped()
        {
            owner.playbackStopped();
        }

        //==============================================================================
        void resetState()
        {
            usesBuffering = false;
            bufferedRegions.clear();
            playerBufferingInProgress = false;

            pendingSeekRequest = false;

            playSpeedMult = 1.0;
            hasAudioFocus = false;
        }

        //==============================================================================
        static LocalRef<jobject> getAudioAttributes()
        {
            auto* env = getEnv();

            auto audioAttribsBuilder = LocalRef<jobject> (env->NewObject (AndroidAudioAttributesBuilder,
                                                                          AndroidAudioAttributesBuilder.constructor));
            static constexpr jint contentTypeMovie = 3;
            static constexpr jint usageMedia = 1;

            LocalRef<jobject> (env->CallObjectMethod (audioAttribsBuilder, AndroidAudioAttributesBuilder.setContentType, contentTypeMovie));
            LocalRef<jobject> (env->CallObjectMethod (audioAttribsBuilder, AndroidAudioAttributesBuilder.setUsage, usageMedia));

            return LocalRef<jobject> (env->CallObjectMethod (audioAttribsBuilder, AndroidAudioAttributesBuilder.build));
        }
    };

   #if JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME
    //==============================================================================
    class SystemVolumeListener
    {
    public:
        SystemVolumeListener (Pimpl& ownerToUse)
            : owner (ownerToUse),
              nativeObserver (createCallbackObject())
        {
            setEnabled (true);
        }

        ~SystemVolumeListener()
        {
            setEnabled (false);
        }
    private:
        Pimpl& owner;
        GlobalRef nativeObserver;

        //==============================================================================
        #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
            METHOD (constructor, "<init>",     "(Landroid/app/Activity;J)V") \
            METHOD (setEnabled,  "setEnabled", "(Z)V")                  \
            CALLBACK (generatedCallback<&SystemVolumeListener::systemVolumeChanged>, "mediaSessionSystemVolumeChanged", "(J)V")

        DECLARE_JNI_CLASS (SystemVolumeObserver, "com/rmsl/juce/SystemVolumeObserver")
        #undef JNI_CLASS_MEMBERS


        LocalRef<jobject> createCallbackObject()
        {
            return LocalRef<jobject> (getEnv()->NewObject (SystemVolumeObserver,
                                                           SystemVolumeObserver.constructor,
                                                           getCurrentActivity().get(),
                                                           reinterpret_cast<jlong> (this)));
        }
    public:
        void setEnabled (bool shouldBeEnabled)
        {
            getEnv()->CallVoidMethod (nativeObserver, SystemVolumeObserver.setEnabled, shouldBeEnabled);

            // Send first notification instantly to ensure sync.
            if (shouldBeEnabled)
                systemVolumeChanged (getEnv(), *this);
        }

    private:
        //==============================================================================
        static void systemVolumeChanged (JNIEnv*, SystemVolumeListener& t)
        {
            MessageManager::callAsync ([weakThis = WeakReference<SystemVolumeListener> { &t }]
                                       {
                                           if (weakThis == nullptr)
                                               return;

                                           NullCheckedInvocation::invoke (weakThis->owner.owner.onGlobalMediaVolumeChanged);
                                       });

        }

        JUCE_DECLARE_WEAK_REFERENCEABLE (SystemVolumeListener)
    };

    //==============================================================================
    VideoComponent& owner;

    MediaSession mediaSession;
    GlobalRef activityLifeListener;
   #if JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME
    SystemVolumeListener systemVolumeListener;
   #endif
    GlobalRef surfaceHolderCallback;

    std::function<void (const URL&, Result)> loadFinishedCallback;

    bool wasOpen = false;

    //==============================================================================
    void loadFinished()
    {
        owner.resized();

        if (loadFinishedCallback != nullptr)
        {
            loadFinishedCallback (currentURL, Result::ok());
            loadFinishedCallback = nullptr;
        }
    }

    void closeVideoFinished()
    {
        owner.resized();
    }

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

    void playbackStarted()
    {
        NullCheckedInvocation::invoke (owner.onPlaybackStarted);
    }

    void playbackStopped()
    {
        NullCheckedInvocation::invoke (owner.onPlaybackStopped);
    }

    //==============================================================================
    void surfaceChanged (LocalRef<jobject> holder, int /*format*/, int /*width*/, int /*height*/) override
    {
        mediaSession.setDisplay (holder);
    }

    void surfaceDestroyed (LocalRef<jobject> /*holder*/) override
    {
        mediaSession.setDisplay (LocalRef<jobject>());
    }

    void surfaceCreated (LocalRef<jobject> /*holder*/) override
    {
    }

    //==============================================================================
    void onActivityPaused (jobject) override
    {
        wasOpen = isOpen();

        if (! wasOpen)
            return;

        JUCE_VIDEO_LOG ("App paused, releasing media player...");

        mediaSession.storeState();
        mediaSession.closeVideo();

       #if JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME
        systemVolumeListener.setEnabled (false);
       #endif
    }

    void onActivityResumed (jobject) override
    {
        if (! wasOpen)
            return;

        JUCE_VIDEO_LOG ("App resumed, restoring media player...");

        loadAsync (currentURL, [this] (const URL&, Result r)
                   {
                       if (r.wasOk())
                           mediaSession.restoreState();
                   });

       #if JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME
        systemVolumeListener.setEnabled (true);
       #endif
    }

    //==============================================================================
   #endif

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl)
};
