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

   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-9-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.

  ==============================================================================
*/
namespace juce
{

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

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

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

DECLARE_JNI_CLASS (AndroidProviderInfo, "android/content/pm/ProviderInfo")
#undef JNI_CLASS_MEMBERS

#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
 METHOD (constructor,       "<init>",            "(Landroid/os/ParcelFileDescriptor;JJ)V") \
 METHOD (createInputStream, "createInputStream", "()Ljava/io/FileInputStream;") \
 METHOD (getLength,         "getLength",         "()J")

DECLARE_JNI_CLASS (AssetFileDescriptor, "android/content/res/AssetFileDescriptor")
#undef JNI_CLASS_MEMBERS

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

DECLARE_JNI_CLASS (JavaCloseable, "java/io/Closeable")
#undef JNI_CLASS_MEMBERS

#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
 STATICMETHOD (open, "open", "(Ljava/io/File;I)Landroid/os/ParcelFileDescriptor;")

DECLARE_JNI_CLASS (ParcelFileDescriptor, "android/os/ParcelFileDescriptor")
#undef JNI_CLASS_MEMBERS

//==============================================================================
class AndroidContentSharerCursor
{
public:
    AndroidContentSharerCursor (JNIEnv* env,
                                const LocalRef<jobject>& contentProvider,
                                const LocalRef<jobjectArray>& resultColumns,
                                std::function<void (AndroidContentSharerCursor&)> onCloseIn)
        : onClose (std::move (onCloseIn)),
          cursor (GlobalRef (LocalRef<jobject> (env->NewObject (JuceContentProviderCursor,
                                                                JuceContentProviderCursor.constructor,
                                                                reinterpret_cast<jlong> (this),
                                                                resultColumns.get()))))
    {
        // the content provider must be created first
        jassert (contentProvider.get() != nullptr);
    }

    jobject getNativeCursor() const { return cursor.get(); }

    void addRow (LocalRef<jobjectArray>& values)
    {
        auto* env = getEnv();

        env->CallVoidMethod (cursor.get(), JuceContentProviderCursor.addRow, values.get());
    }

private:
    static void cursorClosed (JNIEnv*, AndroidContentSharerCursor& t)
    {
        MessageManager::callAsync ([&t]
                                   {
                                       NullCheckedInvocation::invoke (t.onClose, t);
                                   });
    }

    std::function<void (AndroidContentSharerCursor&)> onClose;
    GlobalRef cursor;

    //==============================================================================
    #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
     METHOD (addRow,      "addRow", "([Ljava/lang/Object;)V") \
     METHOD (constructor, "<init>", "(J[Ljava/lang/String;)V") \
     CALLBACK (generatedCallback<&AndroidContentSharerCursor::cursorClosed>, "contentSharerCursorClosed", "(J)V") \

    DECLARE_JNI_CLASS_WITH_BYTECODE (JuceContentProviderCursor, "com/rmsl/juce/JuceContentProviderCursor", 24, javaJuceContentProviderCursor)
    #undef JNI_CLASS_MEMBERS
};

//==============================================================================
class AndroidContentSharerFileObserver
{
public:
    AndroidContentSharerFileObserver (JNIEnv* env,
                                      const LocalRef<jobject>& contentProvider,
                                      const File& filepathToUse,
                                      std::function<void()> onCloseIn)
        : onClose (std::move (onCloseIn)),
          filepath (filepathToUse),
          fileObserver (GlobalRef (LocalRef<jobject> (env->NewObject (JuceContentProviderFileObserver,
                                                                      JuceContentProviderFileObserver.constructor,
                                                                      reinterpret_cast<jlong> (this),
                                                                      javaString (filepath.getFullPathName()).get(),
                                                                      open | access | closeWrite | closeNoWrite))))
    {
        // the content provider must be created first
        jassert (contentProvider.get() != nullptr);

        env->CallVoidMethod (fileObserver, JuceContentProviderFileObserver.startWatching);
    }

    void onFileEvent (int event, const LocalRef<jstring>&)
    {
        if (event == open)
        {
            ++numOpenedHandles;
        }
        else if (event == access)
        {
            fileWasRead = true;
        }
        else if (event == closeNoWrite || event == closeWrite)
        {
            --numOpenedHandles;

            // numOpenedHandles may get negative if we don't receive open handle event
            if (fileWasRead && numOpenedHandles <= 0)
            {
                MessageManager::callAsync ([fo = fileObserver, oc = onClose]
                {
                    getEnv()->CallVoidMethod (fo, JuceContentProviderFileObserver.stopWatching);
                    NullCheckedInvocation::invoke (oc);
                });
            }
        }
    }

private:
    static constexpr int open = 32;
    static constexpr int access = 1;
    static constexpr int closeWrite = 8;
    static constexpr int closeNoWrite = 16;

    std::function<void()> onClose;
    bool fileWasRead = false;
    int numOpenedHandles = 0;

    File filepath;
    GlobalRef fileObserver;

    //==============================================================================
    #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
     METHOD (constructor,   "<init>",        "(JLjava/lang/String;I)V") \
     METHOD (startWatching, "startWatching", "()V") \
     METHOD (stopWatching,  "stopWatching",  "()V") \
     CALLBACK (generatedCallback<&AndroidContentSharerFileObserver::onFileEventCallback>, "contentSharerFileObserverEvent", "(JILjava/lang/String;)V") \

    DECLARE_JNI_CLASS_WITH_BYTECODE (JuceContentProviderFileObserver, "com/rmsl/juce/JuceContentProviderFileObserver", 24, javaJuceContentProviderFileObserver)
    #undef JNI_CLASS_MEMBERS

    static void onFileEventCallback (JNIEnv*, AndroidContentSharerFileObserver& t, jint event, jstring path)
    {
        t.onFileEvent (event, LocalRef<jstring> (path));
    }
};

//==============================================================================
class ContentSharerGlobalImpl
{
public:
    static ContentSharerGlobalImpl& getInstance()
    {
        static ContentSharerGlobalImpl result;
        return result;
    }

    const String packageName = juceString (LocalRef<jstring> ((jstring) getEnv()->CallObjectMethod (getAppContext().get(),
                                                                                                    AndroidContext.getPackageName)));
    const String uriBase = "content://" + packageName + ".sharingcontentprovider/";

    std::unique_ptr<ActivityLauncher> sharePreparedFiles (const std::map<String, File>& fileForUriIn,
                                                          const StringArray& mimeTypes,
                                                          std::function<void (bool)> callback)
    {
        // This function should be called from the main thread, but must not race with singleton
        // access from other threads.
        const ScopedLock lock { mutex };

        if (! isContentSharingEnabled())
        {
            // You need to enable "Content Sharing" in Projucer's Android exporter.
            jassertfalse;
            NullCheckedInvocation::invoke (callback, false);
            return {};
        }

        auto* env = getEnv();

        fileForUri.insert (fileForUriIn.begin(), fileForUriIn.end());

        const auto* action = fileForUriIn.size() == 1 ? "android.intent.action.SEND"
                                                      : "android.intent.action.SEND_MULTIPLE";

        LocalRef<jobject> intent (env->NewObject (AndroidIntent, AndroidIntent.constructor));
        LocalRef { env->CallObjectMethod (intent, AndroidIntent.setAction, javaString (action).get()) };

        LocalRef { env->CallObjectMethod (intent,
                                          AndroidIntent.setType,
                                          javaString (getCommonMimeType (mimeTypes)).get()) };

        constexpr jint grantReadUriPermission   = 1;
        constexpr jint grantPrefixUriPermission = 128;

        LocalRef { env->CallObjectMethod (intent,
                                          AndroidIntent.setFlags,
                                          grantReadUriPermission | grantPrefixUriPermission) };

        if (fileForUriIn.size() == 1)
        {
            const auto uri = fileForUriIn.begin()->first;
            LocalRef<jobject> androidUri { env->CallStaticObjectMethod (AndroidUri, AndroidUri.parse, javaString (uri).get()) };
            LocalRef { env->CallObjectMethod (intent,
                                              AndroidIntent.putExtraParcelable,
                                              javaString ("android.intent.extra.STREAM").get(),
                                              androidUri.get()) };
        }
        else
        {
            LocalRef<jobject> fileUris (env->NewObject (JavaArrayList, JavaArrayList.constructor, fileForUriIn.size()));

            for (const auto& pair : fileForUriIn)
            {
                env->CallBooleanMethod (fileUris,
                                        JavaArrayList.add,
                                        env->CallStaticObjectMethod (AndroidUri,
                                                                     AndroidUri.parse,
                                                                     javaString (pair.first).get()));
            }

            LocalRef { env->CallObjectMethod (intent,
                                              AndroidIntent.putParcelableArrayListExtra,
                                              javaString ("android.intent.extra.STREAM").get(),
                                              fileUris.get()) };
        }

        return doIntent (intent, callback);
    }

    std::unique_ptr<ActivityLauncher> shareText (const String& text,
                                                 std::function<void (bool)> callback)
    {
        // This function should be called from the main thread, but must not race with singleton
        // access from other threads.
        const ScopedLock lock { mutex };

        if (! isContentSharingEnabled())
        {
            // You need to enable "Content Sharing" in Projucer's Android exporter.
            jassertfalse;
            NullCheckedInvocation::invoke (callback, false);
            return {};
        }

        auto* env = getEnv();

        LocalRef<jobject> intent (env->NewObject (AndroidIntent, AndroidIntent.constructor));
        LocalRef { env->CallObjectMethod (intent,
                                          AndroidIntent.setAction,
                                          javaString ("android.intent.action.SEND").get()) };
        LocalRef { env->CallObjectMethod (intent,
                                          AndroidIntent.putExtra,
                                          javaString ("android.intent.extra.TEXT").get(),
                                          javaString (text).get()) };
        LocalRef { env->CallObjectMethod (intent, AndroidIntent.setType, javaString ("text/plain").get()) };

        return doIntent (intent, callback);
    }

    static void onBroadcastResultReceive (JNIEnv*, jobject, int requestCode)
    {
        getInstance().sharingFinished (requestCode, true);
    }

    static jobject JNICALL contentSharerQuery (JNIEnv*, jobject contentProvider, jobject uri, jobjectArray projection)
    {
        return getInstance().query (LocalRef<jobject> (static_cast<jobject> (contentProvider)),
                                    LocalRef<jobject> (static_cast<jobject> (uri)),
                                    LocalRef<jobjectArray> (static_cast<jobjectArray> (projection)));
    }

    static jobject JNICALL contentSharerOpenFile (JNIEnv*, jobject contentProvider, jobject uri, jstring mode)
    {
        return getInstance().openFile (LocalRef<jobject> (static_cast<jobject> (contentProvider)),
                                       LocalRef<jobject> (static_cast<jobject> (uri)),
                                       LocalRef<jstring> (static_cast<jstring> (mode)));
    }

    static jobjectArray JNICALL contentSharerGetStreamTypes (JNIEnv*, jobject /*contentProvider*/, jobject uri, jstring mimeTypeFilter)
    {
        return getInstance().getStreamTypes (LocalRef (uri, IncrementRef::yes),
                                             LocalRef (mimeTypeFilter, IncrementRef::yes));
    }

private:
    ContentSharerGlobalImpl() = default;

    LocalRef<jobject> makeChooser (const LocalRef<jobject>& intent, int request) const
    {
        auto* env = getEnv();

        const auto text = javaString ("Choose share target");

        constexpr jint FLAG_UPDATE_CURRENT = 0x08000000;
        constexpr jint FLAG_IMMUTABLE = 0x04000000;

        const auto context = getAppContext();

        LocalRef<jclass> klass { env->FindClass ("com/rmsl/juce/Receiver") };
        const LocalRef<jobject> replyIntent (env->NewObject (AndroidIntent, AndroidIntent.constructorWithContextAndClass, context.get(), klass.get()));
        LocalRef { getEnv()->CallObjectMethod (replyIntent,
                                               AndroidIntent.putExtraInt,
                                               javaString ("com.rmsl.juce.JUCE_REQUEST_CODE").get(),
                                               request) };

        const auto flags = FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE;
        const LocalRef<jobject> pendingIntent (env->CallStaticObjectMethod (AndroidPendingIntent,
                                                                            AndroidPendingIntent.getBroadcast,
                                                                            context.get(),
                                                                            request,
                                                                            replyIntent.get(),
                                                                            flags));

        return LocalRef<jobject> (env->CallStaticObjectMethod (AndroidIntent,
                                                               AndroidIntent.createChooserWithSender,
                                                               intent.get(),
                                                               text.get(),
                                                               LocalRef { env->CallObjectMethod (pendingIntent,
                                                                                                 AndroidPendingIntent.getIntentSender) }.get()));
    }

    //==============================================================================
    jobject openFile (const LocalRef<jobject>& contentProvider,
                      const LocalRef<jobject>& uri,
                      [[maybe_unused]] const LocalRef<jstring>& mode)
    {
        // This function can be called from multiple threads.
        const ScopedLock lock { mutex };

        auto* env = getEnv();

        auto uriElements = getContentUriElements (env, uri);

        if (uriElements.file == File())
            return nullptr;

        return getAssetFileDescriptor (env, contentProvider, uriElements.file);
    }

    jobject query (const LocalRef<jobject>& contentProvider,
                   const LocalRef<jobject>& uri,
                   const LocalRef<jobjectArray>& projection)
    {
        // This function can be called from multiple threads.
        const ScopedLock lock { mutex };

        StringArray requestedColumns = javaStringArrayToJuce (projection);
        StringArray supportedColumns = getSupportedColumns();

        StringArray resultColumns;

        for (const auto& col : supportedColumns)
        {
            if (requestedColumns.contains (col))
                resultColumns.add (col);
        }

        // Unsupported columns were queried, file sharing may fail.
        if (resultColumns.isEmpty())
            return nullptr;

        auto resultJavaColumns = juceStringArrayToJava (resultColumns);

        auto* env = getEnv();

        const auto uriElements = getContentUriElements (env, uri);

        const auto callback = [info = uriElements.file] (auto& ref)
        {
            auto& pimplCursors = ContentSharerGlobalImpl::getInstance().cursors;
            const auto iter = std::lower_bound (pimplCursors.begin(), pimplCursors.end(), &ref, [] (const auto& managed, const auto* ptr)
            {
                return managed.get() == ptr;
            });

            if (iter != pimplCursors.end() && iter->get() == &ref)
                pimplCursors.erase (iter);
        };

        auto [iter, inserted] = cursors.emplace (new AndroidContentSharerCursor (env,
                                                                                 contentProvider,
                                                                                 resultJavaColumns,
                                                                                 callback));

        if (uriElements.file == File())
            return (*iter)->getNativeCursor();

        LocalRef<jobjectArray> values (env->NewObjectArray ((jsize) resultColumns.size(), JavaObject, nullptr));

        for (int i = 0; i < resultColumns.size(); ++i)
        {
            if (resultColumns.getReference (i) == "_display_name")
            {
                env->SetObjectArrayElement (values, i, javaString (uriElements.filename).get());
            }
            else if (resultColumns.getReference (i) == "_size")
            {
                LocalRef<jobject> javaFile (env->NewObject (JavaFile,
                                                            JavaFile.constructor,
                                                            javaString (uriElements.file.getFullPathName()).get()));

                jlong fileLength = env->CallLongMethod (javaFile, JavaFile.length);

                env->SetObjectArrayElement (values, i, env->NewObject (JavaLong, JavaLong.constructor, fileLength));
            }
        }

        (*iter)->addRow (values);
        return (*iter)->getNativeCursor();
    }

    jobjectArray getStreamTypes (const LocalRef<jobject>& uri, const LocalRef<jstring>& mimeTypeFilter)
    {
        // This function can be called from multiple threads.
        const ScopedLock lock { mutex };

        auto* env = getEnv();

        auto extension = getContentUriElements (env, uri).filename.fromLastOccurrenceOf (".", false, true);

        if (extension.isEmpty())
            return nullptr;

        return juceStringArrayToJava (filterMimeTypes (detail::MimeTypeTable::getMimeTypesForFileExtension (extension),
                                                       juceString (mimeTypeFilter.get()))).release();
    }

    std::unique_ptr<ActivityLauncher> doIntent (const LocalRef<jobject>& intent,
                                                std::function<void (bool)> callback)
    {
        static std::atomic<int> lastRequest = 1003;
        const auto requestCode = lastRequest++;
        callbackForRequest.emplace (requestCode, callback);
        const auto chooser = makeChooser (intent, requestCode);

        auto launcher = std::make_unique<ActivityLauncher> (chooser, requestCode);
        launcher->callback = [] (int request, int resultCode, LocalRef<jobject>)
        {
            ContentSharerGlobalImpl::getInstance().sharingFinished (request, resultCode == -1);
        };
        launcher->open();
        return launcher;
    }

    void sharingFinished (int request, bool succeeded)
    {
        // This function should be called from the main thread, but must not race with singleton
        // access from other threads.
        const ScopedLock lock { mutex };

        const auto iter = callbackForRequest.find (request);

        if (iter == callbackForRequest.end())
            return;

        const ScopeGuard scope { [&] { callbackForRequest.erase (iter); } };

        if (iter->second == nullptr)
            return;

        iter->second (succeeded);
    }

    bool isContentSharingEnabled() const
    {
        auto* env = getEnv();

        LocalRef<jobject> packageManager (env->CallObjectMethod (getAppContext().get(), AndroidContext.getPackageManager));

        constexpr int getProviders = 8;
        LocalRef<jobject> packageInfo (env->CallObjectMethod (packageManager,
                                                              AndroidPackageManager.getPackageInfo,
                                                              javaString (packageName).get(),
                                                              getProviders));
        LocalRef<jobjectArray> providers  ((jobjectArray) env->GetObjectField (packageInfo,
                                                                               AndroidPackageInfo.providers));

        if (providers == nullptr)
            return false;

        auto sharingContentProviderAuthority = packageName + ".sharingcontentprovider";
        const int numProviders = env->GetArrayLength (providers.get());

        for (int i = 0; i < numProviders; ++i)
        {
            LocalRef<jobject> providerInfo (env->GetObjectArrayElement (providers, i));
            LocalRef<jstring> authority ((jstring) env->GetObjectField (providerInfo, AndroidProviderInfo.authority));

            if (juceString (authority) == sharingContentProviderAuthority)
                return true;
        }

        return false;
    }

    //==============================================================================
    struct ContentUriElements
    {
        String filename;
        File file;
    };

    ContentUriElements getContentUriElements (JNIEnv* env, const LocalRef<jobject>& uri) const
    {
        const auto fullUri = juceString ((jstring) env->CallObjectMethod (uri.get(), AndroidUri.toString));

        const auto filename = fullUri.fromLastOccurrenceOf ("/", false, true);

        const auto iter = fileForUri.find (fullUri);
        const auto info = iter != fileForUri.end() ? iter->second : File{};

        return { filename, info };
    }

    static StringArray getSupportedColumns()
    {
        return StringArray ("_display_name", "_size");
    }

    jobject getAssetFileDescriptor (JNIEnv* env, const LocalRef<jobject>& contentProvider, const File& filepath)
    {
        if (nonAssetFilePathsPendingShare.find (filepath) == nonAssetFilePathsPendingShare.end())
        {
            const auto onCloseCallback = [filepath]
            {
                ContentSharerGlobalImpl::getInstance().nonAssetFilePathsPendingShare.erase (filepath);
            };

            auto observer = rawToUniquePtr (new AndroidContentSharerFileObserver (env,
                                                                                  contentProvider,
                                                                                  filepath,
                                                                                  onCloseCallback));

            nonAssetFilePathsPendingShare.emplace (filepath, std::move (observer));
        }

        const LocalRef<jobject> javaFile (env->NewObject (JavaFile,
                                                          JavaFile.constructor,
                                                          javaString (filepath.getFullPathName()).get()));

        constexpr int modeReadOnly = 268435456;
        LocalRef<jobject> parcelFileDescriptor (env->CallStaticObjectMethod (ParcelFileDescriptor,
                                                                             ParcelFileDescriptor.open,
                                                                             javaFile.get(),
                                                                             modeReadOnly));

        if (jniCheckHasExceptionOccurredAndClear())
        {
            // Failed to create file descriptor. Have you provided a valid file path/resource name?
            jassertfalse;
            return nullptr;
        }

        jlong startOffset = 0;
        jlong unknownLength = -1;

        assetFileDescriptors.add (GlobalRef (LocalRef<jobject> (env->NewObject (AssetFileDescriptor,
                                                                                AssetFileDescriptor.constructor,
                                                                                parcelFileDescriptor.get(),
                                                                                startOffset,
                                                                                unknownLength))));

        return assetFileDescriptors.getReference (assetFileDescriptors.size() - 1).get();
    }

    StringArray filterMimeTypes (const StringArray& mimeTypes, const String& filter)
    {
        String filterToUse (filter.removeCharacters ("*"));

        if (filterToUse.isEmpty() || filterToUse == "/")
            return mimeTypes;

        StringArray result;

        for (const auto& type : mimeTypes)
            if (String (type).contains (filterToUse))
                result.add (type);

        return result;
    }

    static String getCommonMimeType (const StringArray& mimeTypes)
    {
        if (mimeTypes.isEmpty())
            return "*/*";

        auto commonMime = mimeTypes[0];
        bool lookForCommonGroup = false;

        for (int i = 1; i < mimeTypes.size(); ++i)
        {
            if (mimeTypes[i] == commonMime)
                continue;

            if (! lookForCommonGroup)
            {
                lookForCommonGroup = true;
                commonMime = commonMime.upToFirstOccurrenceOf ("/", true, false);
            }

            if (! mimeTypes[i].startsWith (commonMime))
                return "*/*";
        }

        return lookForCommonGroup ? commonMime + "*" : commonMime;
    }

    CriticalSection mutex;
    Array<GlobalRef> assetFileDescriptors;
    std::map<File, std::unique_ptr<AndroidContentSharerFileObserver>> nonAssetFilePathsPendingShare;
    std::set<std::unique_ptr<AndroidContentSharerCursor>> cursors;
    std::map<String, File> fileForUri;
    std::map<int, std::function<void (bool)>> callbackForRequest;
};

#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
     CALLBACK (ContentSharerGlobalImpl::contentSharerQuery,          "contentSharerQuery",          "(Landroid/net/Uri;[Ljava/lang/String;)Landroid/database/Cursor;") \
     CALLBACK (ContentSharerGlobalImpl::contentSharerOpenFile,       "contentSharerOpenFile",       "(Landroid/net/Uri;Ljava/lang/String;)Landroid/content/res/AssetFileDescriptor;") \
     CALLBACK (ContentSharerGlobalImpl::contentSharerGetStreamTypes, "contentSharerGetStreamTypes", "(Landroid/net/Uri;Ljava/lang/String;)[Ljava/lang/String;") \

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

#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
     CALLBACK (ContentSharerGlobalImpl::onBroadcastResultReceive, "onBroadcastResultNative", "(I)V")

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

    //==============================================================================
class AndroidContentSharerPrepareFilesTask final : private AsyncUpdater
{
public:
    AndroidContentSharerPrepareFilesTask (const Array<URL>& fileUrls,
                                          std::function<void (const std::map<String, File>&, const StringArray&)> onCompletionIn)
        : onCompletion (std::move (onCompletionIn)),
          task (std::async (std::launch::async, [this, fileUrls]
          {
              run (fileUrls);
              triggerAsyncUpdate();
          })) {}

    ~AndroidContentSharerPrepareFilesTask() override
    {
        task.wait();
        cancelPendingUpdate();
    }

private:
    const String packageName = ContentSharerGlobalImpl::getInstance().packageName;
    const String uriBase     = ContentSharerGlobalImpl::getInstance().uriBase;

    struct StreamCloser
    {
        explicit StreamCloser (const LocalRef<jobject>& streamToUse)
            : stream (GlobalRef (streamToUse))
        {
        }

        ~StreamCloser()
        {
            if (stream.get() != nullptr)
                getEnv()->CallVoidMethod (stream, JavaCloseable.close);
        }

        GlobalRef stream;
    };

    void handleAsyncUpdate() override
    {
        onCompletion (infoForUri, mimeTypes);
    }

    void run (const Array<URL>& fileUrls)
    {
        auto* env = getEnv();

        StringArray filePaths;

        for (const auto& f : fileUrls)
        {
            const auto scheme = f.getScheme();

            // Only "file://" scheme or no scheme (for files in app bundle) are allowed!
            jassert (scheme.isEmpty() || scheme == "file");

            const auto fileToPrepare = [&]
            {
                if (! scheme.isEmpty())
                    return f;

                // Raw resource names need to be all lower case
                jassert (f.toString (true).toLowerCase() == f.toString (true));

                // This will get us a file with file:// URI
                return copyAssetFileToTemporaryFile (env, f.toString (true));
            }();

            if (fileToPrepare.isEmpty())
                continue;

            const auto filepath = URL::removeEscapeChars (fileToPrepare.toString (true).fromFirstOccurrenceOf ("file://", false, false));

            filePaths.add (filepath);
        }

        std::vector<String> extensions;

        for (const auto& filepath : filePaths)
        {
            const auto filename = filepath.fromLastOccurrenceOf ("/", false, true);
            extensions.push_back (filename.fromLastOccurrenceOf (".", false, true));
        }

        std::set<String> mimes;

        if (std::none_of (extensions.begin(), extensions.end(), [] (const String& s) { return s.isEmpty(); }))
            for (const auto& extension : extensions)
                for (const auto& mime : detail::MimeTypeTable::getMimeTypesForFileExtension (extension))
                    mimes.insert (mime);

        for (const auto& mime : mimes)
            mimeTypes.add (mime);

        for (auto it = filePaths.begin(); it != filePaths.end(); ++it)
        {
            const auto filename = it->fromLastOccurrenceOf ("/", false, true);
            const auto contentString = uriBase + String (std::distance (filePaths.begin(), it)) + "/" + filename;
            infoForUri.emplace (contentString, *it);
        }
    }

    URL copyAssetFileToTemporaryFile (JNIEnv* env, const String& filename)
    {
        LocalRef<jobject> resources (env->CallObjectMethod (getAppContext().get(), AndroidContext.getResources));
        int fileId = env->CallIntMethod (resources,
                                         AndroidResources.getIdentifier,
                                         javaString (filename).get(),
                                         javaString ("raw").get(),
                                         javaString (packageName).get());

        // Raw resource not found. Please make sure that you include your file as a raw resource
        // and that you specify just the file name, without an extension.
        jassert (fileId != 0);

        if (fileId == 0)
            return {};

        LocalRef<jobject> assetFd (env->CallObjectMethod (resources,
                                                          AndroidResources.openRawResourceFd,
                                                          fileId));

        StreamCloser inputStream (LocalRef<jobject> (env->CallObjectMethod (assetFd, AssetFileDescriptor.createInputStream)));

        if (jniCheckHasExceptionOccurredAndClear())
        {
            // Failed to open file stream for resource
            jassertfalse;
            return {};
        }

        auto tempFile = File::createTempFile ({});
        tempFile.createDirectory();
        tempFile = tempFile.getChildFile (filename);

        StreamCloser outputStream  (LocalRef<jobject> (env->NewObject (JavaFileOutputStream,
                                                                       JavaFileOutputStream.constructor,
                                                                       javaString (tempFile.getFullPathName()).get())));

        if (jniCheckHasExceptionOccurredAndClear())
        {
            // Failed to open file stream for temporary file
            jassertfalse;
            return {};
        }

        LocalRef<jbyteArray> buffer (env->NewByteArray (1024));
        int bytesRead = 0;

        for (;;)
        {
            bytesRead = env->CallIntMethod (inputStream.stream, JavaFileInputStream.read, buffer.get());

            if (jniCheckHasExceptionOccurredAndClear())
            {
                // Failed to read from resource file.
                jassertfalse;
                return {};
            }

            if (bytesRead < 0)
                break;

            env->CallVoidMethod (outputStream.stream, JavaFileOutputStream.write, buffer.get(), 0, bytesRead);

            if (jniCheckHasExceptionOccurredAndClear())
            {
                // Failed to write to temporary file.
                jassertfalse;
                return {};
            }
        }

        return URL (tempFile);
    }

    std::map<String, File> infoForUri;
    StringArray mimeTypes;
    std::function<void (const std::map<String, File>&, const StringArray&)> onCompletion;
    // This task is obtained from std::async(). Its destructor will block until the asynchronous
    // task has completed; as a result, we can guarantee that the async task will have finished
    // before the lifetimes of the other data members and base class end.
    std::future<void> task;
};

auto detail::ScopedContentSharerInterface::shareFiles (const Array<URL>& urls, Component*) -> std::unique_ptr<ScopedContentSharerInterface>
{
    class NativeScopedContentSharerInterface final : public detail::ScopedContentSharerInterface
    {
    public:
        explicit NativeScopedContentSharerInterface (Array<URL> f)
            : files (std::move (f)) {}

        void runAsync (ContentSharer::Callback callback) override
        {
            // This lambda will only be called if the AndroidContentSharerPrepareFilesTask is still
            // alive. We know that our lifetime will end after the
            // AndroidContentSharerPrepareFilesTask, so there's no need to check that 'this' is
            // still valid inside the lambda.
            task.emplace (files, [this, callback] (const std::map<String, File>& infoForUri, const StringArray& mimeTypes)
            {
                launcher = ContentSharerGlobalImpl::getInstance().sharePreparedFiles (infoForUri, mimeTypes, [callback] (bool success)
                {
                    callback (success, {});
                });
            });
        }

        void close() override
        {
            // dismiss() doesn't close the sharesheet, and there doesn't seem to be any alternative
            // Maybe this will work in the future...
            launcher.reset();
        }

    private:
        Array<URL> files;
        std::optional<AndroidContentSharerPrepareFilesTask> task;
        std::unique_ptr<ActivityLauncher> launcher;
    };

    return std::make_unique<NativeScopedContentSharerInterface> (std::move (urls));
}

auto detail::ScopedContentSharerInterface::shareText (const String& text, Component*) -> std::unique_ptr<ScopedContentSharerInterface>
{
    class NativeScopedContentSharerInterface final : public detail::ScopedContentSharerInterface
    {
    public:
        explicit NativeScopedContentSharerInterface (String t)
            : text (std::move (t)) {}

        void runAsync (ContentSharer::Callback callback) override
        {
            launcher = ContentSharerGlobalImpl::getInstance().shareText (text, [callback] (bool success)
            {
                callback (success, {});
            });
        }

        void close() override
        {
            // dismiss() doesn't close the sharesheet, and there doesn't seem to be any alternative
            // Maybe this will work in the future...
            launcher.reset();
        }

    private:
        String text;
        std::unique_ptr<ActivityLauncher> launcher;
    };

    return std::make_unique<NativeScopedContentSharerInterface> (std::move (text));
}

} // namespace juce
