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

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

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

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

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

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

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

  ==============================================================================
*/
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:
    class Owner
    {
    public:
        virtual ~Owner() {}

        virtual void cursorClosed (const AndroidContentSharerCursor&) = 0;
    };

    AndroidContentSharerCursor (Owner& ownerToUse, JNIEnv* env,
                                const LocalRef<jobject>& contentProvider,
                                const LocalRef<jobjectArray>& resultColumns)
        : owner (ownerToUse),
          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() { return cursor.get(); }

    void cursorClosed()
    {
        MessageManager::callAsync ([this] { owner.cursorClosed (*this); });
    }

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

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

private:
    Owner& owner;
    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 (contentSharerCursorClosed, "contentSharerCursorClosed", "(J)V") \

    DECLARE_JNI_CLASS_WITH_BYTECODE (JuceContentProviderCursor, "com/rmsl/juce/JuceContentProviderCursor", 16, javaJuceContentProviderCursor, sizeof (javaJuceContentProviderCursor))
    #undef JNI_CLASS_MEMBERS

    static void JNICALL contentSharerCursorClosed (JNIEnv*, jobject, jlong host)
    {
        if (auto* myself = reinterpret_cast<AndroidContentSharerCursor*> (host))
            myself->cursorClosed();
    }
};

AndroidContentSharerCursor::JuceContentProviderCursor_Class AndroidContentSharerCursor::JuceContentProviderCursor;

//==============================================================================
class AndroidContentSharerFileObserver
{
public:
    class Owner
    {
    public:
        virtual ~Owner() {}

        virtual void fileHandleClosed (const AndroidContentSharerFileObserver&) = 0;
    };

    AndroidContentSharerFileObserver (Owner& ownerToUse, JNIEnv* env,
                                      const LocalRef<jobject>& contentProvider,
                                      const String& filepathToUse)
        : owner (ownerToUse),
          filepath (filepathToUse),
          fileObserver (GlobalRef (LocalRef<jobject> (env->NewObject (JuceContentProviderFileObserver,
                                                                      JuceContentProviderFileObserver.constructor,
                                                                      reinterpret_cast<jlong> (this),
                                                                      javaString (filepath).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>& path)
    {
        ignoreUnused (path);

        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 ([this]
                {
                    getEnv()->CallVoidMethod (fileObserver, JuceContentProviderFileObserver.stopWatching);
                    owner.fileHandleClosed (*this);
                });
            }
        }
    }

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

    bool fileWasRead = false;
    int numOpenedHandles = 0;

    Owner& owner;
    String 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 (contentSharerFileObserverEvent, "contentSharerFileObserverEvent", "(JILjava/lang/String;)V") \

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

    static void JNICALL contentSharerFileObserverEvent (JNIEnv*, jobject /*fileObserver*/, jlong host, int event, jstring path)
    {
        if (auto* myself = reinterpret_cast<AndroidContentSharerFileObserver*> (host))
            myself->onFileEvent (event, LocalRef<jstring> (path));
    }
};

AndroidContentSharerFileObserver::JuceContentProviderFileObserver_Class AndroidContentSharerFileObserver::JuceContentProviderFileObserver;

//==============================================================================
class AndroidContentSharerPrepareFilesThread    : private Thread
{
public:
    AndroidContentSharerPrepareFilesThread (AsyncUpdater& ownerToUse,
                                            const Array<URL>& fileUrlsToUse,
                                            const String& packageNameToUse,
                                            const String& uriBaseToUse)
        : Thread ("AndroidContentSharerPrepareFilesThread"),
          owner (ownerToUse),
          fileUrls (fileUrlsToUse),
          resultFileUris (GlobalRef (LocalRef<jobject> (getEnv()->NewObject (JavaArrayList,
                                                                             JavaArrayList.constructor,
                                                                             fileUrls.size())))),
          packageName (packageNameToUse),
          uriBase (uriBaseToUse)
    {
        startThread();
    }

    ~AndroidContentSharerPrepareFilesThread() override
    {
        signalThreadShouldExit();
        waitForThreadToExit (10000);

        for (auto& f : temporaryFilesFromAssetFiles)
            f.deleteFile();
    }

    jobject getResultFileUris()  { return resultFileUris.get(); }
    const StringArray& getMimeTypes() const { return mimeTypes; }
    const StringArray& getFilePaths() const { return filePaths; }

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

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

        GlobalRef stream;
    };

    void run() override
    {
        auto* env = getEnv();

        bool canSpecifyMimeTypes = true;

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

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

            if (scheme.isEmpty())
            {
                // 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
                f = copyAssetFileToTemporaryFile (env, f.toString (true));

                if (f.isEmpty())
                    continue;
            }

            if (threadShouldExit())
                return;

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

            filePaths.add (filepath);

            auto filename = filepath.fromLastOccurrenceOf ("/", false, true);
            auto fileExtension = filename.fromLastOccurrenceOf (".", false, true);
            auto contentString = uriBase + String (filePaths.size() - 1) + "/" + filename;

            auto uri = LocalRef<jobject> (env->CallStaticObjectMethod (AndroidUri, AndroidUri.parse,
                                                                       javaString (contentString).get()));

            if (canSpecifyMimeTypes)
                canSpecifyMimeTypes = fileExtension.isNotEmpty();

            if (canSpecifyMimeTypes)
                mimeTypes.addArray (MimeTypeTable::getMimeTypesForFileExtension (fileExtension));
            else
                mimeTypes.clear();

            env->CallBooleanMethod (resultFileUris, JavaArrayList.add, uri.get());
        }

        owner.triggerAsyncUpdate();
    }

    URL copyAssetFileToTemporaryFile (JNIEnv* env, const String& filename)
    {
        auto resources = LocalRef<jobject> (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 {};

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

        auto inputStream = StreamCloser (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);

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

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

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

        for (;;)
        {
            if (threadShouldExit())
                return {};

            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 {};
            }
        }

        temporaryFilesFromAssetFiles.add (tempFile);

        return URL (tempFile);
    }

    AsyncUpdater& owner;
    Array<URL> fileUrls;

    GlobalRef resultFileUris;
    String packageName;
    String uriBase;

    StringArray filePaths;
    Array<File> temporaryFilesFromAssetFiles;
    StringArray mimeTypes;
};

//==============================================================================
class ContentSharer::ContentSharerNativeImpl  : public ContentSharer::Pimpl,
                                                public AndroidContentSharerFileObserver::Owner,
                                                public AndroidContentSharerCursor::Owner,
                                                public AsyncUpdater,
                                                private Timer
{
public:
    ContentSharerNativeImpl (ContentSharer& cs)
        : owner (cs),
          packageName (juceString (LocalRef<jstring> ((jstring) getEnv()->CallObjectMethod (getAppContext().get(), AndroidContext.getPackageName)))),
          uriBase ("content://" + packageName + ".sharingcontentprovider/")
    {
    }

    ~ContentSharerNativeImpl() override
    {
        masterReference.clear();
    }

    void shareFiles (const Array<URL>& files) override
    {
        if (! isContentSharingEnabled())
        {
            // You need to enable "Content Sharing" in Projucer's Android exporter.
            jassertfalse;
            owner.sharingFinished (false, {});
        }

        prepareFilesThread.reset (new AndroidContentSharerPrepareFilesThread (*this, files, packageName, uriBase));
    }

    void shareText (const String& text) override
    {
        if (! isContentSharingEnabled())
        {
            // You need to enable "Content Sharing" in Projucer's Android exporter.
            jassertfalse;
            owner.sharingFinished (false, {});
        }

        auto* env = getEnv();

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

        auto chooserIntent = LocalRef<jobject> (env->CallStaticObjectMethod (AndroidIntent, AndroidIntent.createChooser,
                                                                             intent.get(), javaString ("Choose share target").get()));

        startAndroidActivityForResult (chooserIntent, 1003,
                                       [weakRef = WeakReference<ContentSharerNativeImpl> { this }] (int /*requestCode*/,
                                                                                                    int resultCode,
                                                                                                    LocalRef<jobject> /*intentData*/) mutable
                                       {
                                           if (weakRef != nullptr)
                                               weakRef->sharingFinished (resultCode);
                                       });
    }

    //==============================================================================
    void cursorClosed (const AndroidContentSharerCursor& cursor) override
    {
        cursors.removeObject (&cursor);
    }

    void fileHandleClosed (const AndroidContentSharerFileObserver&) override
    {
        decrementPendingFileCountAndNotifyOwnerIfReady();
    }

    //==============================================================================
    jobject openFile (const LocalRef<jobject>& contentProvider,
                      const LocalRef<jobject>& uri, const LocalRef<jstring>& mode)
    {
        ignoreUnused (mode);

        WeakReference<ContentSharerNativeImpl> weakRef (this);

        if (weakRef == nullptr)
            return nullptr;

        auto* env = getEnv();

        auto uriElements = getContentUriElements (env, uri);

        if (uriElements.filepath.isEmpty())
            return nullptr;

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

    jobject query (const LocalRef<jobject>& contentProvider, const LocalRef<jobject>& uri,
                   const LocalRef<jobjectArray>& projection)
    {
        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();

        auto cursor = cursors.add (new AndroidContentSharerCursor (*this, env, contentProvider,
                                                                   resultJavaColumns));

        auto uriElements = getContentUriElements (env, uri);

        if (uriElements.filepath.isEmpty())
            return cursor->getNativeCursor();

        auto values = LocalRef<jobjectArray> (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")
            {
                auto javaFile = LocalRef<jobject> (env->NewObject (JavaFile, JavaFile.constructor,
                                                                   javaString (uriElements.filepath).get()));

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

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

        cursor->addRow (values);
        return cursor->getNativeCursor();
    }

    jobjectArray getStreamTypes (const LocalRef<jobject>& uri, const LocalRef<jstring>& mimeTypeFilter)
    {
        auto* env = getEnv();

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

        if (extension.isEmpty())
            return nullptr;

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

    void sharingFinished (int resultCode)
    {
        sharingActivityDidFinish = true;

        succeeded = resultCode == -1;

        // Give content sharer a chance to request file access.
        if (nonAssetFilesPendingShare.get() == 0)
            startTimer (2000);
        else
            notifyOwnerIfReady();
    }

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

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

        constexpr int getProviders = 8;
        auto packageInfo = LocalRef<jobject> (env->CallObjectMethod (packageManager,
                                                                     AndroidPackageManager.getPackageInfo,
                                                                     javaString (packageName).get(),
                                                                     getProviders));
        auto providers = LocalRef<jobjectArray> ((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)
        {
            auto providerInfo = LocalRef<jobject> (env->GetObjectArrayElement (providers, i));
            auto authority = LocalRef<jstring> ((jstring) env->GetObjectField (providerInfo,
                                                                               AndroidProviderInfo.authority));

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

        return false;
    }

    void handleAsyncUpdate() override
    {
        jassert (prepareFilesThread != nullptr);

        if (prepareFilesThread == nullptr)
            return;

        filesPrepared (prepareFilesThread->getResultFileUris(), prepareFilesThread->getMimeTypes());
    }

    void filesPrepared (jobject fileUris, const StringArray& mimeTypes)
    {
        auto* env = getEnv();

        auto intent = LocalRef<jobject> (env->NewObject (AndroidIntent, AndroidIntent.constructor));
        env->CallObjectMethod (intent, AndroidIntent.setAction,
                               javaString ("android.intent.action.SEND_MULTIPLE").get());

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

        constexpr int grantReadPermission = 1;
        env->CallObjectMethod (intent, AndroidIntent.setFlags, grantReadPermission);

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

        auto chooserIntent = LocalRef<jobject> (env->CallStaticObjectMethod (AndroidIntent,
                                                                             AndroidIntent.createChooser,
                                                                             intent.get(),
                                                                             javaString ("Choose share target").get()));

        startAndroidActivityForResult (chooserIntent, 1003,
                                       [weakRef = WeakReference<ContentSharerNativeImpl> { this }] (int /*requestCode*/,
                                                                                                    int resultCode,
                                                                                                    LocalRef<jobject> /*intentData*/) mutable
                                       {
                                           if (weakRef != nullptr)
                                               weakRef->sharingFinished (resultCode);
                                       });
    }

    void decrementPendingFileCountAndNotifyOwnerIfReady()
    {
        --nonAssetFilesPendingShare;

        notifyOwnerIfReady();
    }

    void notifyOwnerIfReady()
    {
        if (sharingActivityDidFinish && nonAssetFilesPendingShare.get() == 0)
            owner.sharingFinished (succeeded, {});
    }

    void timerCallback() override
    {
        stopTimer();

        notifyOwnerIfReady();
    }

    //==============================================================================
    struct ContentUriElements
    {
        String index;
        String filename;
        String filepath;
    };

    ContentUriElements getContentUriElements (JNIEnv* env, const LocalRef<jobject>& uri) const
    {
        jassert (prepareFilesThread != nullptr);

        if (prepareFilesThread == nullptr)
            return {};

        auto fullUri = juceString ((jstring) env->CallObjectMethod (uri.get(), AndroidUri.toString));

        auto index = fullUri.fromFirstOccurrenceOf (uriBase, false, false)
                             .upToFirstOccurrenceOf ("/", false, true);

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

        return { index, filename, prepareFilesThread->getFilePaths()[index.getIntValue()] };
    }

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

    jobject getAssetFileDescriptor (JNIEnv* env, const LocalRef<jobject>& contentProvider,
                                  const String& filepath)
    {
        // This function can be called from multiple threads.
        {
            const ScopedLock sl (nonAssetFileOpenLock);

            if (! nonAssetFilePathsPendingShare.contains (filepath))
            {
                nonAssetFilePathsPendingShare.add (filepath);
                ++nonAssetFilesPendingShare;

                nonAssetFileObservers.add (new AndroidContentSharerFileObserver (*this, env,
                                                                                 contentProvider,
                                                                                 filepath));
            }
        }

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

        constexpr int modeReadOnly = 268435456;
        auto parcelFileDescriptor = LocalRef<jobject> (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;
    }

    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;
    }

    ContentSharer& owner;
    String packageName;
    String uriBase;

    std::unique_ptr<AndroidContentSharerPrepareFilesThread> prepareFilesThread;

    bool succeeded = false;
    String errorDescription;

    bool sharingActivityDidFinish = false;

    OwnedArray<AndroidContentSharerCursor> cursors;

    Array<GlobalRef> assetFileDescriptors;

    CriticalSection nonAssetFileOpenLock;
    StringArray nonAssetFilePathsPendingShare;
    Atomic<int> nonAssetFilesPendingShare { 0 };
    OwnedArray<AndroidContentSharerFileObserver> nonAssetFileObservers;

    WeakReference<ContentSharerNativeImpl>::Master masterReference;
    friend class WeakReference<ContentSharerNativeImpl>;

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

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

    static jobject JNICALL contentSharerQuery (JNIEnv*, jobject contentProvider, jobject uri, jobjectArray projection)
    {
        if (auto *pimpl = (ContentSharer::ContentSharerNativeImpl *) ContentSharer::getInstance ()->pimpl.get ())
            return pimpl->query (LocalRef<jobject> (static_cast<jobject> (contentProvider)),
                                 LocalRef<jobject> (static_cast<jobject> (uri)),
                                 LocalRef<jobjectArray> (static_cast<jobjectArray> (projection)));

        return nullptr;
    }

    static jobject JNICALL contentSharerOpenFile (JNIEnv*, jobject contentProvider, jobject uri, jstring mode)
    {
        if (auto* pimpl = (ContentSharer::ContentSharerNativeImpl*) ContentSharer::getInstance()->pimpl.get())
            return pimpl->openFile (LocalRef<jobject> (static_cast<jobject> (contentProvider)),
                                    LocalRef<jobject> (static_cast<jobject> (uri)),
                                    LocalRef<jstring> (static_cast<jstring> (mode)));

        return nullptr;
    }

    static jobjectArray JNICALL contentSharerGetStreamTypes (JNIEnv*, jobject /*contentProvider*/, jobject uri, jstring mimeTypeFilter)
    {
        if (auto* pimpl = (ContentSharer::ContentSharerNativeImpl*) ContentSharer::getInstance()->pimpl.get())
            return pimpl->getStreamTypes (LocalRef<jobject> (static_cast<jobject> (uri)),
                                          LocalRef<jstring> (static_cast<jstring> (mimeTypeFilter)));

        return nullptr;
    }
};

//==============================================================================
ContentSharer::Pimpl* ContentSharer::createPimpl()
{
    return new ContentSharerNativeImpl (*this);
}

ContentSharer::ContentSharerNativeImpl::JuceSharingContentProvider_Class ContentSharer::ContentSharerNativeImpl::JuceSharingContentProvider;

} // namespace juce
