diff --git a/BREAKING-CHANGES.txt b/BREAKING-CHANGES.txt index 79728c8a70..af295b317c 100644 --- a/BREAKING-CHANGES.txt +++ b/BREAKING-CHANGES.txt @@ -4,6 +4,32 @@ JUCE breaking changes develop ======= +Change +------ +The signatures of the ContentSharer member functions have been updated. The +ContentSharer class itself is no longer a singleton. + +Possible Issues +--------------- +Projects that use the old signatures will not build until they are updated. + +Workaround +---------- +Instead of calling content sharer functions through a singleton instance, e.g. + ContentSharer::getInstance()->shareText (...); +call the static member functions directly: + ScopedMessageBox messageBox = ContentSharer::shareTextScoped (...); +The new functions return a ScopedMessageBox instance. On iOS, the content +sharer will only remain open for as long as the ScopedMessageBox remains alive. +On Android, this functionality will be added as/when the native APIs allow. + +Rationale +--------- +The new signatures are safer and easier to use. The ScopedMessageBox also +allows content sharers to be dismissed programmatically, which wasn't +previously possible. + + Change ------ The minimum supported AAX library version has been bumped to 2.4.0 and the diff --git a/examples/DemoRunner/CMakeLists.txt b/examples/DemoRunner/CMakeLists.txt index 140d829e44..01cf615f75 100644 --- a/examples/DemoRunner/CMakeLists.txt +++ b/examples/DemoRunner/CMakeLists.txt @@ -54,6 +54,7 @@ target_sources(DemoRunner PRIVATE target_compile_definitions(DemoRunner PRIVATE PIP_JUCE_EXAMPLES_DIRECTORY_STRING="${JUCE_SOURCE_DIR}/examples" JUCE_ALLOW_STATIC_NULL_VARIABLES=0 + JUCE_CONTENT_SHARING=1 JUCE_DEMO_RUNNER=1 JUCE_PLUGINHOST_LV2=1 JUCE_PLUGINHOST_VST3=1 diff --git a/examples/GUI/CameraDemo.h b/examples/GUI/CameraDemo.h index 14e64c2873..f49eed85a7 100644 --- a/examples/GUI/CameraDemo.h +++ b/examples/GUI/CameraDemo.h @@ -314,12 +314,11 @@ private: SafePointer safeThis (this); - juce::ContentSharer::getInstance()->shareFiles ({url}, - [safeThis] (bool success, const String&) mutable - { - if (safeThis) - safeThis->sharingFinished (success, false); - }); + messageBox = ContentSharer::shareFilesScoped ({ url }, [safeThis] (bool success, const String&) + { + if (safeThis) + safeThis->sharingFinished (success, false); + }); #endif } } @@ -355,12 +354,11 @@ private: SafePointer safeThis (this); - juce::ContentSharer::getInstance()->shareFiles ({url}, - [safeThis] (bool success, const String&) mutable - { - if (safeThis) - safeThis->sharingFinished (success, true); - }); + messageBox = ContentSharer::shareFilesScoped ({ url }, [safeThis] (bool success, const String&) + { + if (safeThis) + safeThis->sharingFinished (success, true); + }); } #endif } diff --git a/examples/GUI/DialogsDemo.h b/examples/GUI/DialogsDemo.h index 31fc48100b..ea090af31f 100644 --- a/examples/GUI/DialogsDemo.h +++ b/examples/GUI/DialogsDemo.h @@ -462,7 +462,7 @@ private: } else if (type == shareText) { - ContentSharer::getInstance()->shareText ("I love JUCE!", [ptr = Component::SafePointer (this)] (bool success, const String& error) + messageBox = ContentSharer::shareTextScoped ("I love JUCE!", [ptr = Component::SafePointer (this)] (bool success, const String& error) { if (ptr == nullptr) return; @@ -489,7 +489,7 @@ private: Array urls; urls.add (URL (fileToSave)); - ContentSharer::getInstance()->shareFiles (urls, [ptr = Component::SafePointer (this)] (bool success, const String& error) + messageBox = ContentSharer::shareFilesScoped (urls, [ptr = Component::SafePointer (this)] (bool success, const String& error) { if (ptr == nullptr) return; @@ -519,7 +519,7 @@ private: Array images { myImage, myImage2 }; - ContentSharer::getInstance()->shareImages (images, [ptr = Component::SafePointer (this)] (bool success, const String& error) + messageBox = ContentSharer::shareImagesScoped (images, nullptr, [ptr = Component::SafePointer (this)] (bool success, const String& error) { if (ptr == nullptr) return; diff --git a/extras/AudioPluginHost/CMakeLists.txt b/extras/AudioPluginHost/CMakeLists.txt index cd7f495e8e..b38dfd9dbf 100644 --- a/extras/AudioPluginHost/CMakeLists.txt +++ b/extras/AudioPluginHost/CMakeLists.txt @@ -48,6 +48,7 @@ juce_add_binary_data(AudioPluginHostData SOURCES target_compile_definitions(AudioPluginHost PRIVATE JUCE_ALSA=1 + JUCE_CONTENT_SHARING=1 JUCE_DIRECTSOUND=1 JUCE_DISABLE_CAUTIOUS_PARAMETER_ID_CHECKING=1 JUCE_PLUGINHOST_LADSPA=1 diff --git a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h index e2b2713d03..b3ab038a36 100644 --- a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h +++ b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h @@ -1675,6 +1675,10 @@ private: if (! isLibrary()) { + auto* receiver = getOrCreateChildWithName (application, "receiver"); + setAttributeIfNotPresent (*receiver, "android:name", "com.rmsl.juce.Receiver"); + setAttributeIfNotPresent (*receiver, "android:exported", "false"); + auto* app = createApplicationElement (*manifest); auto* act = createActivityElement (*app); diff --git a/modules/juce_core/native/juce_android_JNIHelpers.cpp b/modules/juce_core/native/juce_android_JNIHelpers.cpp index 45eb542f43..6f677c575c 100644 --- a/modules/juce_core/native/juce_android_JNIHelpers.cpp +++ b/modules/juce_core/native/juce_android_JNIHelpers.cpp @@ -94,8 +94,8 @@ struct SystemJavaClassComparator if ((! isSysClassA) && (! isSysClassB)) { - return DefaultElementComparator::compareElements (first != nullptr ? first->byteCode != nullptr : false, - second != nullptr ? second->byteCode != nullptr : false); + return DefaultElementComparator::compareElements (first != nullptr && first->byteCode != nullptr, + second != nullptr && second->byteCode != nullptr); } return DefaultElementComparator::compareElements (isSystemClass (first), @@ -631,43 +631,17 @@ jobject FragmentOverlay::getNativeHandle() } //============================================================================== -class ActivityLauncher : public FragmentOverlay +void startAndroidActivityForResult (const LocalRef& intent, + int requestCode, + std::function)>&& callback) { -public: - ActivityLauncher (const LocalRef& intentToUse, - int requestCodeToUse, - std::function)> && callbackToUse) - : intent (intentToUse), requestCode (requestCodeToUse), callback (std::move (callbackToUse)) - {} - - void onStart() override + auto* launcher = new ActivityLauncher (intent, requestCode); + launcher->callback = [launcher, c = std::move (callback)] (auto&&... args) { - if (! std::exchange (activityHasStarted, true)) - getEnv()->CallVoidMethod (getNativeHandle(), AndroidFragment.startActivityForResult, - intent.get(), requestCode); - } - - void onActivityResult (int activityRequestCode, int resultCode, LocalRef data) override - { - if (callback) - callback (activityRequestCode, resultCode, std::move (data)); - - getEnv()->CallVoidMethod (getNativeHandle(), JuceFragmentOverlay.close); - delete this; - } - -private: - GlobalRef intent; - int requestCode; - std::function)> callback; - bool activityHasStarted = false; -}; - -void startAndroidActivityForResult (const LocalRef& intent, int requestCode, - std::function)> && callback) -{ - auto* activityLauncher = new ActivityLauncher (intent, requestCode, std::move (callback)); - activityLauncher->open(); + NullCheckedInvocation::invoke (c, args...); + delete launcher; + }; + launcher->open(); } //============================================================================== diff --git a/modules/juce_core/native/juce_android_JNIHelpers.h b/modules/juce_core/native/juce_android_JNIHelpers.h index aeb49f97b2..54cc785397 100644 --- a/modules/juce_core/native/juce_android_JNIHelpers.h +++ b/modules/juce_core/native/juce_android_JNIHelpers.h @@ -262,7 +262,8 @@ template constexpr auto numBytes (const T (&) [N]) { retu METHOD (getApplicationInfo, "getApplicationInfo", "()Landroid/content/pm/ApplicationInfo;") \ METHOD (checkCallingOrSelfPermission, "checkCallingOrSelfPermission", "(Ljava/lang/String;)I") \ METHOD (checkCallingOrSelfUriPermission, "checkCallingOrSelfUriPermission", "(Landroid/net/Uri;I)I") \ - METHOD (getCacheDir, "getCacheDir", "()Ljava/io/File;") + METHOD (getCacheDir, "getCacheDir", "()Ljava/io/File;") \ + METHOD (registerReceiver, "registerReceiver", "(Landroid/content/BroadcastReceiver;Landroid/content/IntentFilter;)Landroid/content/Intent;") \ DECLARE_JNI_CLASS (AndroidContext, "android/content/Context") #undef JNI_CLASS_MEMBERS @@ -418,6 +419,7 @@ DECLARE_JNI_CLASS (AndroidHandlerThread, "android/os/HandlerThread") METHOD (putExtraStrings, "putExtra", "(Ljava/lang/String;[Ljava/lang/String;)Landroid/content/Intent;") \ METHOD (putExtraParcelable, "putExtra", "(Ljava/lang/String;Landroid/os/Parcelable;)Landroid/content/Intent;") \ METHOD (putExtraBool, "putExtra", "(Ljava/lang/String;Z)Landroid/content/Intent;") \ + METHOD (putExtraInt, "putExtra", "(Ljava/lang/String;I)Landroid/content/Intent;") \ METHOD (putParcelableArrayListExtra, "putParcelableArrayListExtra", "(Ljava/lang/String;Ljava/util/ArrayList;)Landroid/content/Intent;") \ METHOD (setAction, "setAction", "(Ljava/lang/String;)Landroid/content/Intent;") \ METHOD (setFlags, "setFlags", "(I)Landroid/content/Intent;") \ @@ -427,6 +429,12 @@ DECLARE_JNI_CLASS (AndroidHandlerThread, "android/os/HandlerThread") DECLARE_JNI_CLASS (AndroidIntent, "android/content/Intent") #undef JNI_CLASS_MEMBERS +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ + STATICMETHOD (createChooser, "createChooser", "(Landroid/content/Intent;Ljava/lang/CharSequence;Landroid/content/IntentSender;)Landroid/content/Intent;") \ + +DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidIntent22, "android/content/Intent", 22) +#undef JNI_CLASS_MEMBERS + #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (constructor, "", "()V") \ METHOD (postRotate, "postRotate", "(FFF)Z") \ @@ -490,7 +498,8 @@ DECLARE_JNI_CLASS (AndroidPaint, "android/graphics/Paint") #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ STATICMETHOD (getActivity, "getActivity", "(Landroid/content/Context;ILandroid/content/Intent;I)Landroid/app/PendingIntent;") \ - METHOD (getIntentSender, "getIntentSender", "()Landroid/content/IntentSender;") + STATICMETHOD (getBroadcast, "getBroadcast", "(Landroid/content/Context;ILandroid/content/Intent;I)Landroid/app/PendingIntent;") \ + METHOD (getIntentSender, "getIntentSender", "()Landroid/content/IntentSender;") \ DECLARE_JNI_CLASS (AndroidPendingIntent, "android/app/PendingIntent") #undef JNI_CLASS_MEMBERS @@ -1026,10 +1035,37 @@ private: //============================================================================== // Allows you to start an activity without requiring to have an activity -void startAndroidActivityForResult (const LocalRef& intent, int requestCode, - std::function)> && callback); +void startAndroidActivityForResult (const LocalRef& intent, + int requestCode, + std::function)>&& callback); -//============================================================================== +class ActivityLauncher : public FragmentOverlay +{ +public: + ActivityLauncher (const LocalRef& intentToUse, int requestCodeToUse) + : intent (intentToUse), requestCode (requestCodeToUse) + {} + + void onStart() override + { + if (! std::exchange (activityHasStarted, true)) + getEnv()->CallVoidMethod (getNativeHandle(), AndroidFragment.startActivityForResult, intent.get(), requestCode); + } + + void onActivityResult (int activityRequestCode, int resultCode, LocalRef data) override + { + NullCheckedInvocation::invoke (callback, activityRequestCode, resultCode, std::move (data)); + } + + std::function)> callback; + +private: + GlobalRef intent; + int requestCode; + bool activityHasStarted = false; +}; + + //============================================================================== bool androidHasSystemFeature (const String& property); String audioManagerGetProperty (const String& property); diff --git a/modules/juce_gui_basics/detail/juce_ScopedContentSharerImpl.h b/modules/juce_gui_basics/detail/juce_ScopedContentSharerImpl.h new file mode 100644 index 0000000000..c4cc158e5b --- /dev/null +++ b/modules/juce_gui_basics/detail/juce_ScopedContentSharerImpl.h @@ -0,0 +1,98 @@ +/* + ============================================================================== + + 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::detail +{ + +class ConcreteScopedContentSharerImpl : public ScopedMessageBoxImpl, + private AsyncUpdater +{ +public: + static ScopedMessageBox show (std::unique_ptr&& native, + ContentSharer::Callback callback) + { + return ScopedMessageBox (runAsync (std::move (native), std::move (callback))); + } + + ~ConcreteScopedContentSharerImpl() override + { + cancelPendingUpdate(); + } + + void close() override + { + cancelPendingUpdate(); + nativeImplementation->close(); + self.reset(); + } + +private: + static std::shared_ptr runAsync (std::unique_ptr&& p, + ContentSharer::Callback&& c) + { + std::shared_ptr result (new ConcreteScopedContentSharerImpl (std::move (p), std::move (c))); + result->self = result; + result->triggerAsyncUpdate(); + return result; + } + + ConcreteScopedContentSharerImpl (std::unique_ptr&& p, + ContentSharer::Callback&& c) + : callback (std::move (c)), nativeImplementation (std::move (p)) {} + + void handleAsyncUpdate() override + { + nativeImplementation->runAsync ([weakRecipient = std::weak_ptr (self)] (bool result, const String& error) + { + const auto notifyRecipient = [result, error, weakRecipient] + { + if (const auto locked = weakRecipient.lock()) + { + NullCheckedInvocation::invoke (locked->callback, result, error); + locked->self.reset(); + } + }; + + if (MessageManager::getInstance()->isThisTheMessageThread()) + notifyRecipient(); + else + MessageManager::callAsync (notifyRecipient); + }); + } + + ContentSharer::Callback callback; + std::unique_ptr nativeImplementation; + + /* The 'old' native message box API doesn't have a concept of content sharer owners. + Instead, content sharers have to clean up after themselves, once they're done displaying. + To allow this mode of usage, the implementation keeps an owning reference to itself, + which is cleared once the content sharer is closed or asked to quit. To display a content + sharer box without a scoped lifetime, just create a Pimpl instance without using + the ScopedContentSharer wrapper, and the Pimpl will destroy itself after it is dismissed. + */ + std::shared_ptr self; +}; + +} // namespace juce::detail diff --git a/modules/juce_gui_basics/detail/juce_ScopedContentSharerInterface.h b/modules/juce_gui_basics/detail/juce_ScopedContentSharerInterface.h new file mode 100644 index 0000000000..5e847ee4fd --- /dev/null +++ b/modules/juce_gui_basics/detail/juce_ScopedContentSharerInterface.h @@ -0,0 +1,211 @@ +/* + ============================================================================== + + 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::detail +{ + +/* + Instances of this type can show and dismiss a content sharer. + + This is an interface rather than a concrete type so that platforms can pick an implementation at + runtime if necessary. +*/ +struct ScopedContentSharerInterface +{ + virtual ~ScopedContentSharerInterface() = default; + + /* Shows the content sharer. + + When the content sharer exits normally, it should send the result to the passed-in function. + The passed-in function is safe to call from any thread at any time. + */ + virtual void runAsync (ContentSharer::Callback callback) + { + jassertfalse; + NullCheckedInvocation::invoke (callback, false, "Content sharing not available on this platform!"); + } + + /* Forcefully closes the content sharer. + + This will be called when the content sharer handle has fallen out of scope. + If the content sharer has already been closed by the user, this shouldn't do anything. + */ + virtual void close() {} + + /* Implemented differently for each platform. */ + static std::unique_ptr shareFiles (const Array&, Component*); + static std::unique_ptr shareText (const String&, Component*); + + /* Implemented below. */ + static std::unique_ptr shareImages (const Array&, std::unique_ptr, Component*); + static std::unique_ptr shareData (MemoryBlock, Component*); +}; + +class TemporaryFilesDecorator : public ScopedContentSharerInterface, + private AsyncUpdater +{ +public: + explicit TemporaryFilesDecorator (Component* parentIn) + : parent (parentIn) {} + + void runAsync (ContentSharer::Callback cb) override + { + callback = std::move (cb); + + task = std::async (std::launch::async, [this] + { + std::tie (temporaryFiles, error) = prepareTemporaryFiles(); + triggerAsyncUpdate(); + }); + } + + void close() override + { + if (inner != nullptr) + inner->close(); + } + +private: + virtual std::tuple, String> prepareTemporaryFiles() const = 0; + + void handleAsyncUpdate() override + { + if (error.isNotEmpty()) + { + NullCheckedInvocation::invoke (callback, false, error); + return; + } + + inner = shareFiles (temporaryFiles, parent); + + if (inner == nullptr) + { + NullCheckedInvocation::invoke (callback, false, TRANS ("Failed to create file sharer")); + return; + } + + inner->runAsync (callback); + } + + Array temporaryFiles; + String error; + std::unique_ptr inner; + ContentSharer::Callback callback; + std::future task; + Component* parent = nullptr; +}; + +std::unique_ptr ScopedContentSharerInterface::shareImages (const Array& images, + std::unique_ptr format, + Component* parent) +{ + class Decorator : public TemporaryFilesDecorator + { + public: + Decorator (Array imagesIn, std::unique_ptr formatIn, Component* parentIn) + : TemporaryFilesDecorator (parentIn), images (std::move (imagesIn)), format (std::move (formatIn)) {} + + private: + std::tuple, String> prepareTemporaryFiles() const override + { + const auto extension = format->getFormatName().toLowerCase(); + + Array result; + + for (const auto& image : images) + { + File tempFile = File::createTempFile (extension); + + if (! tempFile.create().wasOk()) + return { Array{}, TRANS ("Failed to create temporary file") }; + + std::unique_ptr outputStream (tempFile.createOutputStream()); + + if (outputStream == nullptr) + return { Array{}, TRANS ("Failed to open temporary file for writing") }; + + if (format->writeImageToStream (image, *outputStream)) + result.add (URL (tempFile)); + } + + for (const auto& url : result) + jassertquiet (url.isLocalFile() && url.getLocalFile().existsAsFile()); + + return { std::move (result), String{} }; + } + + Array images; + std::unique_ptr format; + }; + + return std::make_unique (images, + format == nullptr ? std::make_unique() : std::move (format), + parent); +} + +std::unique_ptr ScopedContentSharerInterface::shareData (MemoryBlock mb, Component* parent) +{ + class Decorator : public TemporaryFilesDecorator + { + public: + Decorator (MemoryBlock mbIn, Component* parentIn) + : TemporaryFilesDecorator (parentIn), mb (std::move (mbIn)) {} + + private: + std::tuple, String> prepareTemporaryFiles() const override + { + File tempFile = File::createTempFile ("data"); + + if (! tempFile.create().wasOk()) + return { Array{}, TRANS ("Failed to create temporary file") }; + + std::unique_ptr outputStream (tempFile.createOutputStream()); + + if (outputStream == nullptr) + return { Array{}, TRANS ("Failed to open temporary file for writing") }; + + size_t pos = 0; + size_t totalSize = mb.getSize(); + + while (pos < totalSize) + { + size_t numToWrite = std::min ((size_t) 8192, totalSize - pos); + + if (! outputStream->write (mb.begin() + pos, numToWrite)) + return { Array{}, TRANS ("Failed to write to temporary file") }; + + pos += numToWrite; + } + + return { Array { URL (tempFile) }, String{} }; + } + + MemoryBlock mb; + }; + + return std::make_unique (std::move (mb), parent); +} + +} // namespace juce::detail diff --git a/modules/juce_gui_basics/detail/juce_ScopedMessageBoxImpl.h b/modules/juce_gui_basics/detail/juce_ScopedMessageBoxImpl.h index 661e183a1e..8396ca829f 100644 --- a/modules/juce_gui_basics/detail/juce_ScopedMessageBoxImpl.h +++ b/modules/juce_gui_basics/detail/juce_ScopedMessageBoxImpl.h @@ -27,7 +27,16 @@ namespace juce::detail { //============================================================================== -class ScopedMessageBoxImpl : private AsyncUpdater +class ScopedMessageBoxImpl +{ +public: + virtual ~ScopedMessageBoxImpl() = default; + virtual void close() = 0; +}; + +//============================================================================== +class ConcreteScopedMessageBoxImpl : public ScopedMessageBoxImpl, + private AsyncUpdater { public: static ScopedMessageBox show (std::unique_ptr&& native, @@ -50,12 +59,12 @@ public: return 0; } - ~ScopedMessageBoxImpl() override + ~ConcreteScopedMessageBoxImpl() override { cancelPendingUpdate(); } - void close() + void close() override { cancelPendingUpdate(); nativeImplementation->close(); @@ -63,10 +72,10 @@ public: } private: - static std::shared_ptr runAsync (std::unique_ptr&& p, - std::unique_ptr&& c) + static std::shared_ptr runAsync (std::unique_ptr&& p, + std::unique_ptr&& c) { - std::shared_ptr result (new ScopedMessageBoxImpl (std::move (p), std::move (c))); + std::shared_ptr result (new ConcreteScopedMessageBoxImpl (std::move (p), std::move (c))); result->self = result; result->triggerAsyncUpdate(); return result; @@ -78,16 +87,16 @@ private: return local != nullptr ? local->runSync() : 0; } - explicit ScopedMessageBoxImpl (std::unique_ptr&& p) - : ScopedMessageBoxImpl (std::move (p), nullptr) {} + explicit ConcreteScopedMessageBoxImpl (std::unique_ptr&& p) + : ConcreteScopedMessageBoxImpl (std::move (p), nullptr) {} - ScopedMessageBoxImpl (std::unique_ptr&& p, - std::unique_ptr&& c) + ConcreteScopedMessageBoxImpl (std::unique_ptr&& p, + std::unique_ptr&& c) : callback (std::move (c)), nativeImplementation (std::move (p)) {} void handleAsyncUpdate() override { - nativeImplementation->runAsync ([weakRecipient = std::weak_ptr (self)] (int result) + nativeImplementation->runAsync ([weakRecipient = std::weak_ptr (self)] (int result) { const auto notifyRecipient = [result, weakRecipient] { @@ -117,7 +126,7 @@ private: message box without a scoped lifetime, just create a Pimpl instance without using the ScopedMessageBox wrapper, and the Pimpl will destroy itself after it is dismissed. */ - std::shared_ptr self; + std::shared_ptr self; }; } // namespace juce::detail diff --git a/modules/juce_gui_basics/filebrowser/juce_ContentSharer.cpp b/modules/juce_gui_basics/filebrowser/juce_ContentSharer.cpp index eb1c7514d2..25136a263e 100644 --- a/modules/juce_gui_basics/filebrowser/juce_ContentSharer.cpp +++ b/modules/juce_gui_basics/filebrowser/juce_ContentSharer.cpp @@ -26,247 +26,49 @@ namespace juce { -#if JUCE_CONTENT_SHARING -//============================================================================== -class ContentSharer::PrepareImagesThread : private Thread +ScopedMessageBox ContentSharer::shareFilesScoped (const Array& files, + Callback callback, + Component* parent) { -public: - PrepareImagesThread (ContentSharer& cs, const Array& imagesToUse, - ImageFileFormat* imageFileFormatToUse) - : Thread ("ContentSharer::PrepareImagesThread"), - owner (cs), - images (imagesToUse), - imageFileFormat (imageFileFormatToUse == nullptr ? new PNGImageFormat() - : imageFileFormatToUse), - extension (imageFileFormat->getFormatName().toLowerCase()) - { - startThread(); - } - - ~PrepareImagesThread() override - { - signalThreadShouldExit(); - waitForThreadToExit (10000); - } - -private: - void run() override - { - for (const auto& image : images) - { - if (threadShouldExit()) - return; - - File tempFile = File::createTempFile (extension); - - if (! tempFile.create().wasOk()) - break; - - std::unique_ptr outputStream (tempFile.createOutputStream()); - - if (outputStream == nullptr) - break; - - if (imageFileFormat->writeImageToStream (image, *outputStream)) - owner.temporaryFiles.add (tempFile); - } - - finish(); - } - - void finish() - { - MessageManager::callAsync ([this]() { owner.filesToSharePrepared(); }); - } - - ContentSharer& owner; - const Array images; - std::unique_ptr imageFileFormat; - String extension; -}; - -//============================================================================== -class ContentSharer::PrepareDataThread : private Thread -{ -public: - PrepareDataThread (ContentSharer& cs, const MemoryBlock& mb) - : Thread ("ContentSharer::PrepareDataThread"), - owner (cs), - data (mb) - { - startThread(); - } - - ~PrepareDataThread() override - { - signalThreadShouldExit(); - waitForThreadToExit (10000); - } - -private: - void run() override - { - File tempFile = File::createTempFile ("data"); - - if (tempFile.create().wasOk()) - { - if (auto outputStream = std::unique_ptr (tempFile.createOutputStream())) - { - size_t pos = 0; - size_t totalSize = data.getSize(); - - while (pos < totalSize) - { - if (threadShouldExit()) - return; - - size_t numToWrite = std::min ((size_t) 8192, totalSize - pos); - - outputStream->write (data.begin() + pos, numToWrite); - - pos += numToWrite; - } - - owner.temporaryFiles.add (tempFile); - } - } - - finish(); - } - - void finish() - { - MessageManager::callAsync ([this]() { owner.filesToSharePrepared(); }); - } - - ContentSharer& owner; - const MemoryBlock data; -}; -#endif - -//============================================================================== -JUCE_IMPLEMENT_SINGLETON (ContentSharer) - -ContentSharer::ContentSharer() {} -ContentSharer::~ContentSharer() { clearSingletonInstance(); } - -void ContentSharer::shareFiles ([[maybe_unused]] const Array& files, - std::function callbackToUse) -{ - #if JUCE_CONTENT_SHARING - startNewShare (callbackToUse); - pimpl->shareFiles (files); - #else - // Content sharing is not available on this platform! - jassertfalse; - - if (callbackToUse) - callbackToUse (false, "Content sharing is not available on this platform!"); - #endif + auto impl = detail::ScopedContentSharerInterface::shareFiles (files, parent); + return detail::ConcreteScopedContentSharerImpl::show (std::move (impl), std::move (callback)); } -#if JUCE_CONTENT_SHARING -void ContentSharer::startNewShare (std::function callbackToUse) +ScopedMessageBox ContentSharer::shareTextScoped (const String& text, + Callback callback, + Component* parent) { - // You should not start another sharing operation before the previous one is finished. - // Forcibly stopping a previous sharing operation is rarely a good idea! - jassert (pimpl == nullptr); - pimpl.reset(); + auto impl = detail::ScopedContentSharerInterface::shareText (text, parent); + return detail::ConcreteScopedContentSharerImpl::show (std::move (impl), std::move (callback)); +} - prepareDataThread = nullptr; - prepareImagesThread = nullptr; +ScopedMessageBox ContentSharer::shareImagesScoped (const Array& images, + std::unique_ptr format, + Callback callback, + Component* parent) +{ + auto impl = detail::ScopedContentSharerInterface::shareImages (images, std::move (format), parent); + return detail::ConcreteScopedContentSharerImpl::show (std::move (impl), std::move (callback)); +} - deleteTemporaryFiles(); +ScopedMessageBox ContentSharer::shareDataScoped (const MemoryBlock& mb, + Callback callback, + Component* parent) +{ + auto impl = detail::ScopedContentSharerInterface::shareData (mb, parent); + return detail::ConcreteScopedContentSharerImpl::show (std::move (impl), std::move (callback)); +} - // You need to pass a valid callback. - jassert (callbackToUse); - callback = std::move (callbackToUse); +#if ! (JUCE_CONTENT_SHARING && (JUCE_IOS || JUCE_ANDROID)) +auto detail::ScopedContentSharerInterface::shareFiles (const Array&, Component*) -> std::unique_ptr +{ + return std::make_unique(); +} - pimpl.reset (createPimpl()); +auto detail::ScopedContentSharerInterface::shareText (const String&, Component*) -> std::unique_ptr +{ + return std::make_unique(); } #endif -void ContentSharer::shareText ([[maybe_unused]] const String& text, - std::function callbackToUse) -{ - #if JUCE_CONTENT_SHARING - startNewShare (callbackToUse); - pimpl->shareText (text); - #else - // Content sharing is not available on this platform! - jassertfalse; - - if (callbackToUse) - callbackToUse (false, "Content sharing is not available on this platform!"); - #endif -} - -void ContentSharer::shareImages ([[maybe_unused]] const Array& images, - std::function callbackToUse, - [[maybe_unused]] ImageFileFormat* imageFileFormatToUse) -{ - #if JUCE_CONTENT_SHARING - startNewShare (callbackToUse); - prepareImagesThread.reset (new PrepareImagesThread (*this, images, imageFileFormatToUse)); - #else - // Content sharing is not available on this platform! - jassertfalse; - - if (callbackToUse) - callbackToUse (false, "Content sharing is not available on this platform!"); - #endif -} - -#if JUCE_CONTENT_SHARING -void ContentSharer::filesToSharePrepared() -{ - Array urls; - - for (const auto& tempFile : temporaryFiles) - urls.add (URL (tempFile)); - - prepareImagesThread = nullptr; - prepareDataThread = nullptr; - - pimpl->shareFiles (urls); -} -#endif - -void ContentSharer::shareData ([[maybe_unused]] const MemoryBlock& mb, - std::function callbackToUse) -{ - #if JUCE_CONTENT_SHARING - startNewShare (callbackToUse); - prepareDataThread.reset (new PrepareDataThread (*this, mb)); - #else - if (callbackToUse) - callbackToUse (false, "Content sharing not available on this platform!"); - #endif -} - -void ContentSharer::sharingFinished (bool succeeded, const String& errorDescription) -{ - deleteTemporaryFiles(); - - std::function cb; - std::swap (cb, callback); - - String error (errorDescription); - - #if JUCE_CONTENT_SHARING - pimpl.reset(); - #endif - - if (cb) - cb (succeeded, error); -} - -void ContentSharer::deleteTemporaryFiles() -{ - for (auto& f : temporaryFiles) - f.deleteFile(); - - temporaryFiles.clear(); -} - } // namespace juce diff --git a/modules/juce_gui_basics/filebrowser/juce_ContentSharer.h b/modules/juce_gui_basics/filebrowser/juce_ContentSharer.h index 1edc79f314..fee6b76fd2 100644 --- a/modules/juce_gui_basics/filebrowser/juce_ContentSharer.h +++ b/modules/juce_gui_basics/filebrowser/juce_ContentSharer.h @@ -23,21 +23,30 @@ ============================================================================== */ -#pragma once - namespace juce { -/** A singleton class responsible for sharing content between apps and devices. +//============================================================================== +/** + Functions that allow sharing content between apps and devices. You can share text, images, files or an arbitrary data block. @tags{GUI} */ -class JUCE_API ContentSharer : public DeletedAtShutdown +class JUCE_API ContentSharer { public: - JUCE_DECLARE_SINGLETON (ContentSharer, false) + ContentSharer() = delete; + + /** A callback of this type is passed when starting a content sharing + session. + + When the session ends, the function will receive a flag indicating + whether the session was successful. In the case of failure, the + errorText argument may hold a string describing the problem. + */ + using Callback = std::function; /** Shares the given files. Each URL should be either a full file path or it should point to a resource within the application bundle. For @@ -50,9 +59,16 @@ public: Sadly on Android the returned success flag may be wrong as there is no standard way the sharing targets report if the sharing operation succeeded. Also, the optional error message is always empty on Android. + + @param files the files to share + @param callback a callback that will be called on the main thread + when the sharing session ends + @param parent the component that should be used to host the + sharing view */ - void shareFiles (const Array& files, - std::function callback); + [[nodiscard]] static ScopedMessageBox shareFilesScoped (const Array& files, + Callback callback, + Component* parent = nullptr); /** Shares the given text. @@ -60,9 +76,16 @@ public: Sadly on Android the returned success flag may be wrong as there is no standard way the sharing targets report if the sharing operation succeeded. Also, the optional error message is always empty on Android. + + @param text the text to share + @param callback a callback that will be called on the main thread + when the sharing session ends + @param parent the component that should be used to host the + sharing view */ - void shareText (const String& text, - std::function callback); + [[nodiscard]] static ScopedMessageBox shareTextScoped (const String& text, + Callback callback, + Component* parent = nullptr); /** A convenience function to share an image. This is useful when you have images loaded in memory. The images will be written to temporary files first, so if @@ -84,10 +107,20 @@ public: Sadly on Android the returned success flag may be wrong as there is no standard way the sharing targets report if the sharing operation succeeded. Also, the optional error message is always empty on Android. + + @param images the images to share + @param format the file format to use when saving the images. + If no format is provided, a sensible default will + be used. + @param callback a callback that will be called on the main thread + when the sharing session ends + @param parent the component that should be used to host the + sharing view */ - void shareImages (const Array& images, - std::function callback, - ImageFileFormat* imageFileFormatToUse = nullptr); + [[nodiscard]] static ScopedMessageBox shareImagesScoped (const Array& images, + std::unique_ptr format, + Callback callback, + Component* parent = nullptr); /** A convenience function to share arbitrary data. The data will be written to a temporary file and then that file will be shared. If you have @@ -97,47 +130,16 @@ public: Sadly on Android the returned success flag may be wrong as there is no standard way the sharing targets report if the sharing operation succeeded. Also, the optional error message is always empty on Android. + + @param mb the data to share + @param callback a callback that will be called on the main thread + when the sharing session ends + @param parent the component that should be used to host the + sharing view */ - void shareData (const MemoryBlock& mb, - std::function callback); - -private: - ContentSharer(); - ~ContentSharer(); - - Array temporaryFiles; - - std::function callback; - - #if JUCE_CONTENT_SHARING - struct Pimpl - { - virtual ~Pimpl() {} - virtual void shareFiles (const Array& files) = 0; - virtual void shareText (const String& text) = 0; - }; - - std::unique_ptr pimpl; - Pimpl* createPimpl(); - - void startNewShare (std::function); - - class ContentSharerNativeImpl; - friend class ContentSharerNativeImpl; - - class PrepareImagesThread; - friend class PrepareImagesThread; - std::unique_ptr prepareImagesThread; - - class PrepareDataThread; - friend class PrepareDataThread; - std::unique_ptr prepareDataThread; - - void filesToSharePrepared(); - #endif - - void deleteTemporaryFiles(); - void sharingFinished (bool, const String&); + [[nodiscard]] static ScopedMessageBox shareDataScoped (const MemoryBlock& mb, + Callback callback, + Component* parent = nullptr); }; } // namespace juce diff --git a/modules/juce_gui_basics/juce_gui_basics.cpp b/modules/juce_gui_basics/juce_gui_basics.cpp index 22f53641d1..a684e6ddeb 100644 --- a/modules/juce_gui_basics/juce_gui_basics.cpp +++ b/modules/juce_gui_basics/juce_gui_basics.cpp @@ -122,6 +122,8 @@ #include "detail/juce_ToolbarItemDragAndDropOverlayComponent.h" #include "detail/juce_ScopedMessageBoxInterface.h" #include "detail/juce_ScopedMessageBoxImpl.h" +#include "detail/juce_ScopedContentSharerInterface.h" +#include "detail/juce_ScopedContentSharerImpl.h" #include "detail/juce_WindowingHelpers.h" #include "detail/juce_AlertWindowHelpers.h" #include "detail/juce_TopLevelWindowManager.h" @@ -143,6 +145,7 @@ #include "native/accessibility/juce_ios_Accessibility.mm" #include "native/juce_ios_Windowing.mm" #include "native/juce_ios_NativeMessageBox.mm" + #include "native/juce_ios_NativeModalWrapperComponent.h" #include "native/juce_ios_FileChooser.mm" #if JUCE_CONTENT_SHARING diff --git a/modules/juce_gui_basics/native/javaopt/app/com/rmsl/juce/Receiver.java b/modules/juce_gui_basics/native/javaopt/app/com/rmsl/juce/Receiver.java new file mode 100644 index 0000000000..0a01d451a9 --- /dev/null +++ b/modules/juce_gui_basics/native/javaopt/app/com/rmsl/juce/Receiver.java @@ -0,0 +1,42 @@ +/* + ============================================================================== + + 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. + + ============================================================================== +*/ + +package com.rmsl.juce; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +//============================================================================== +public class Receiver extends BroadcastReceiver +{ + @Override + public void onReceive (Context context, Intent intent) + { + onBroadcastResultNative (intent.getIntExtra ("com.rmsl.juce.JUCE_REQUEST_CODE", 0)); + } + + private native void onBroadcastResultNative (int requestCode); +} diff --git a/modules/juce_gui_basics/native/juce_android_ContentSharer.cpp b/modules/juce_gui_basics/native/juce_android_ContentSharer.cpp index 9330d28dec..f7b50519fa 100644 --- a/modules/juce_gui_basics/native/juce_android_ContentSharer.cpp +++ b/modules/juce_gui_basics/native/juce_android_ContentSharer.cpp @@ -98,18 +98,11 @@ DECLARE_JNI_CLASS (ParcelFileDescriptor, "android/os/ParcelFileDescriptor") class AndroidContentSharerCursor { public: - class Owner - { - public: - virtual ~Owner() = default; - - virtual void cursorClosed (const AndroidContentSharerCursor&) = 0; - }; - - AndroidContentSharerCursor (Owner& ownerToUse, JNIEnv* env, + AndroidContentSharerCursor (JNIEnv* env, const LocalRef& contentProvider, - const LocalRef& resultColumns) - : owner (ownerToUse), + const LocalRef& resultColumns, + std::function onCloseIn) + : onClose (std::move (onCloseIn)), cursor (GlobalRef (LocalRef (env->NewObject (JuceContentProviderCursor, JuceContentProviderCursor.constructor, reinterpret_cast (this), @@ -119,12 +112,7 @@ public: jassert (contentProvider.get() != nullptr); } - jobject getNativeCursor() { return cursor.get(); } - - static void cursorClosed (JNIEnv*, AndroidContentSharerCursor& t) - { - MessageManager::callAsync ([&t] { t.owner.cursorClosed (t); }); - } + jobject getNativeCursor() const { return cursor.get(); } void addRow (LocalRef& values) { @@ -134,7 +122,15 @@ public: } private: - Owner& owner; + static void cursorClosed (JNIEnv*, AndroidContentSharerCursor& t) + { + MessageManager::callAsync ([&t] + { + NullCheckedInvocation::invoke (t.onClose, t); + }); + } + + std::function onClose; GlobalRef cursor; //============================================================================== @@ -151,23 +147,16 @@ private: class AndroidContentSharerFileObserver { public: - class Owner - { - public: - virtual ~Owner() {} - - virtual void fileHandleClosed (const AndroidContentSharerFileObserver&) = 0; - }; - - AndroidContentSharerFileObserver (Owner& ownerToUse, JNIEnv* env, + AndroidContentSharerFileObserver (JNIEnv* env, const LocalRef& contentProvider, - const String& filepathToUse) - : owner (ownerToUse), + const File& filepathToUse, + std::function onCloseIn) + : onClose (std::move (onCloseIn)), filepath (filepathToUse), fileObserver (GlobalRef (LocalRef (env->NewObject (JuceContentProviderFileObserver, JuceContentProviderFileObserver.constructor, reinterpret_cast (this), - javaString (filepath).get(), + javaString (filepath.getFullPathName()).get(), open | access | closeWrite | closeNoWrite)))) { // the content provider must be created first @@ -176,7 +165,7 @@ public: env->CallVoidMethod (fileObserver, JuceContentProviderFileObserver.startWatching); } - void onFileEvent (int event, [[maybe_unused]] const LocalRef& path) + void onFileEvent (int event, const LocalRef&) { if (event == open) { @@ -193,10 +182,10 @@ public: // numOpenedHandles may get negative if we don't receive open handle event. if (fileWasRead && numOpenedHandles <= 0) { - MessageManager::callAsync ([this] + MessageManager::callAsync ([fileObserver = fileObserver, onClose = onClose] { getEnv()->CallVoidMethod (fileObserver, JuceContentProviderFileObserver.stopWatching); - owner.fileHandleClosed (*this); + NullCheckedInvocation::invoke (onClose); }); } } @@ -208,11 +197,11 @@ private: static constexpr int closeWrite = 8; static constexpr int closeNoWrite = 16; + std::function onClose; bool fileWasRead = false; int numOpenedHandles = 0; - Owner& owner; - String filepath; + File filepath; GlobalRef fileObserver; //============================================================================== @@ -232,294 +221,190 @@ private: }; //============================================================================== -class AndroidContentSharerPrepareFilesThread : private Thread +class ContentSharerGlobalImpl { public: - AndroidContentSharerPrepareFilesThread (AsyncUpdater& ownerToUse, - const Array& fileUrlsToUse, - const String& packageNameToUse, - const String& uriBaseToUse) - : Thread ("AndroidContentSharerPrepareFilesThread"), - owner (ownerToUse), - fileUrls (fileUrlsToUse), - resultFileUris (GlobalRef (LocalRef (getEnv()->NewObject (JavaArrayList, - JavaArrayList.constructor, - fileUrls.size())))), - packageName (packageNameToUse), - uriBase (uriBaseToUse) + static ContentSharerGlobalImpl& getInstance() { - startThread(); + static ContentSharerGlobalImpl result; + return result; } - ~AndroidContentSharerPrepareFilesThread() override + const String packageName = juceString (LocalRef ((jstring) getEnv()->CallObjectMethod (getAppContext().get(), + AndroidContext.getPackageName))); + const String uriBase = "content://" + packageName + ".sharingcontentprovider/"; + + std::unique_ptr sharePreparedFiles (const std::map& fileForUriIn, + const StringArray& mimeTypes, + std::function callback) { - signalThreadShouldExit(); - waitForThreadToExit (10000); + // This function should be called from the main thread, but must not race with singleton + // access from other threads. + const ScopedLock lock { mutex }; - 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& 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 (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 (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 (env->CallObjectMethod (resources, - AndroidResources.openRawResourceFd, - fileId)); - - auto inputStream = StreamCloser (LocalRef (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 (env->NewObject (JavaFileOutputStream, - JavaFileOutputStream.constructor, - javaString (tempFile.getFullPathName()).get()))); - - if (jniCheckHasExceptionOccurredAndClear()) - { - // Failed to open file stream for temporary file - jassertfalse; - return {}; - } - - auto buffer = LocalRef (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 fileUrls; - - GlobalRef resultFileUris; - String packageName; - String uriBase; - - StringArray filePaths; - Array 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) getEnv()->CallObjectMethod (getAppContext().get(), AndroidContext.getPackageName)))), - uriBase ("content://" + packageName + ".sharingcontentprovider/") - { - } - - ~ContentSharerNativeImpl() override - { - masterReference.clear(); - } - - void shareFiles (const Array& 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, {}); + NullCheckedInvocation::invoke (callback, false); + return {}; } auto* env = getEnv(); - auto intent = LocalRef (env->NewObject (AndroidIntent, AndroidIntent.constructor)); - env->CallObjectMethod (intent, AndroidIntent.setAction, + fileForUri.insert (fileForUriIn.begin(), fileForUriIn.end()); + + LocalRef 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 intent (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.get()); + + return doIntent (intent, callback); + } + + std::unique_ptr shareText (const String& text, + std::function 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 intent (env->NewObject (AndroidIntent, AndroidIntent.constructor)); + env->CallObjectMethod (intent, + AndroidIntent.setAction, javaString ("android.intent.action.SEND").get()); - env->CallObjectMethod (intent, AndroidIntent.putExtra, + 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 (env->CallStaticObjectMethod (AndroidIntent, AndroidIntent.createChooser, - intent.get(), javaString ("Choose share target").get())); - - startAndroidActivityForResult (chooserIntent, 1003, - [weakRef = WeakReference { this }] (int /*requestCode*/, - int resultCode, - LocalRef /*intentData*/) mutable - { - if (weakRef != nullptr) - weakRef->sharingFinished (resultCode); - }); + return doIntent (intent, callback); } - //============================================================================== - void cursorClosed (const AndroidContentSharerCursor& cursor) override + static void onBroadcastResultReceive (JNIEnv*, jobject, int requestCode) { - cursors.removeObject (&cursor); + getInstance().sharingFinished (requestCode, true); } - void fileHandleClosed (const AndroidContentSharerFileObserver&) override + static jobject JNICALL contentSharerQuery (JNIEnv*, jobject contentProvider, jobject uri, jobjectArray projection) { - decrementPendingFileCountAndNotifyOwnerIfReady(); + return getInstance().query (LocalRef (static_cast (contentProvider)), + LocalRef (static_cast (uri)), + LocalRef (static_cast (projection))); + } + + static jobject JNICALL contentSharerOpenFile (JNIEnv*, jobject contentProvider, jobject uri, jstring mode) + { + return getInstance().openFile (LocalRef (static_cast (contentProvider)), + LocalRef (static_cast (uri)), + LocalRef (static_cast (mode))); + } + + static jobjectArray JNICALL contentSharerGetStreamTypes (JNIEnv*, jobject /*contentProvider*/, jobject uri, jstring mimeTypeFilter) + { + return getInstance().getStreamTypes (LocalRef (static_cast (uri)), + LocalRef (static_cast (mimeTypeFilter))); + } + +private: + ContentSharerGlobalImpl() = default; + + LocalRef makeChooser (const LocalRef& intent, int request) const + { + auto* env = getEnv(); + + const auto text = javaString ("Choose share target"); + + if (getAndroidSDKVersion() < 22) + return LocalRef (env->CallStaticObjectMethod (AndroidIntent, + AndroidIntent.createChooser, + intent.get(), + text.get())); + + constexpr jint FLAG_UPDATE_CURRENT = 0x08000000; + constexpr jint FLAG_IMMUTABLE = 0x04000000; + + const auto context = getAppContext(); + + auto* klass = env->FindClass ("com/rmsl/juce/Receiver"); + const LocalRef replyIntent (env->NewObject (AndroidIntent, AndroidIntent.constructorWithContextAndClass, context.get(), klass)); + getEnv()->CallObjectMethod (replyIntent, AndroidIntent.putExtraInt, javaString ("com.rmsl.juce.JUCE_REQUEST_CODE").get(), request); + + const auto flags = FLAG_UPDATE_CURRENT | (getAndroidSDKVersion() <= 23 ? 0 : FLAG_IMMUTABLE); + const LocalRef pendingIntent (env->CallStaticObjectMethod (AndroidPendingIntent, + AndroidPendingIntent.getBroadcast, + context.get(), + request, + replyIntent.get(), + flags)); + + return LocalRef (env->CallStaticObjectMethod (AndroidIntent22, + AndroidIntent22.createChooser, + intent.get(), + text.get(), + env->CallObjectMethod (pendingIntent, + AndroidPendingIntent.getIntentSender))); } //============================================================================== jobject openFile (const LocalRef& contentProvider, - const LocalRef& uri, [[maybe_unused]] const LocalRef& mode) + const LocalRef& uri, + [[maybe_unused]] const LocalRef& mode) { - WeakReference weakRef (this); - - if (weakRef == nullptr) - return nullptr; + // This function can be called from multiple threads. + const ScopedLock lock { mutex }; auto* env = getEnv(); auto uriElements = getContentUriElements (env, uri); - if (uriElements.filepath.isEmpty()) + if (uriElements.file == File()) return nullptr; - return getAssetFileDescriptor (env, contentProvider, uriElements.filepath); + return getAssetFileDescriptor (env, contentProvider, uriElements.file); } - jobject query (const LocalRef& contentProvider, const LocalRef& uri, + jobject query (const LocalRef& contentProvider, + const LocalRef& uri, const LocalRef& projection) { + // This function can be called from multiple threads. + const ScopedLock lock { mutex }; + StringArray requestedColumns = javaStringArrayToJuce (projection); StringArray supportedColumns = getSupportedColumns(); @@ -539,16 +424,29 @@ public: auto* env = getEnv(); - auto cursor = cursors.add (new AndroidContentSharerCursor (*this, env, contentProvider, - resultJavaColumns)); + const auto uriElements = getContentUriElements (env, uri); - 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 (uriElements.filepath.isEmpty()) - return cursor->getNativeCursor(); + if (iter != pimplCursors.end() && iter->get() == &ref) + pimplCursors.erase (iter); + }; - auto values = LocalRef (env->NewObjectArray ((jsize) resultColumns.size(), - JavaObject, nullptr)); + auto [iter, inserted] = cursors.emplace (new AndroidContentSharerCursor (env, + contentProvider, + resultJavaColumns, + callback)); + + if (uriElements.file == File()) + return (*iter)->getNativeCursor(); + + LocalRef values (env->NewObjectArray ((jsize) resultColumns.size(), JavaObject, nullptr)); for (int i = 0; i < resultColumns.size(); ++i) { @@ -558,23 +456,25 @@ public: } else if (resultColumns.getReference (i) == "_size") { - auto javaFile = LocalRef (env->NewObject (JavaFile, JavaFile.constructor, - javaString (uriElements.filepath).get())); + LocalRef 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)); + env->SetObjectArrayElement (values, i, env->NewObject (JavaLong, JavaLong.constructor, fileLength)); } } - cursor->addRow (values); - return cursor->getNativeCursor(); + (*iter)->addRow (values); + return (*iter)->getNativeCursor(); } jobjectArray getStreamTypes (const LocalRef& uri, const LocalRef& 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); @@ -582,24 +482,45 @@ public: if (extension.isEmpty()) return nullptr; - return juceStringArrayToJava (filterMimeTypes (MimeTypeTable::getMimeTypesForFileExtension (extension), - juceString (mimeTypeFilter.get()))); + return juceStringArrayToJava (filterMimeTypes (MimeTypeTable::getMimeTypesForFileExtension (extension), juceString (mimeTypeFilter.get()))); } - void sharingFinished (int resultCode) + std::unique_ptr doIntent (const LocalRef& intent, + std::function callback) { - sharingActivityDidFinish = true; + static std::atomic lastRequest = 1003; + const auto requestCode = lastRequest++; + callbackForRequest.emplace (requestCode, callback); + const auto chooser = makeChooser (intent, requestCode); - succeeded = resultCode == -1; - - // Give content sharer a chance to request file access. - if (nonAssetFilesPendingShare.get() == 0) - startTimer (2000); - else - notifyOwnerIfReady(); + auto launcher = std::make_unique (chooser, requestCode); + launcher->callback = [] (int request, int resultCode, LocalRef) + { + 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); } -private: bool isContentSharingEnabled() const { auto* env = getEnv(); @@ -607,12 +528,12 @@ private: LocalRef packageManager (env->CallObjectMethod (getAppContext().get(), AndroidContext.getPackageManager)); constexpr int getProviders = 8; - auto packageInfo = LocalRef (env->CallObjectMethod (packageManager, - AndroidPackageManager.getPackageInfo, - javaString (packageName).get(), - getProviders)); - auto providers = LocalRef ((jobjectArray) env->GetObjectField (packageInfo, - AndroidPackageInfo.providers)); + LocalRef packageInfo (env->CallObjectMethod (packageManager, + AndroidPackageManager.getPackageInfo, + javaString (packageName).get(), + getProviders)); + LocalRef providers ((jobjectArray) env->GetObjectField (packageInfo, + AndroidPackageInfo.providers)); if (providers == nullptr) return false; @@ -622,9 +543,8 @@ private: for (int i = 0; i < numProviders; ++i) { - auto providerInfo = LocalRef (env->GetObjectArrayElement (providers, i)); - auto authority = LocalRef ((jstring) env->GetObjectField (providerInfo, - AndroidProviderInfo.authority)); + LocalRef providerInfo (env->GetObjectArrayElement (providers, i)); + LocalRef authority ((jstring) env->GetObjectField (providerInfo, AndroidProviderInfo.authority)); if (juceString (authority) == sharingContentProviderAuthority) return true; @@ -633,92 +553,23 @@ private: 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 (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 (env->CallStaticObjectMethod (AndroidIntent, - AndroidIntent.createChooser, - intent.get(), - javaString ("Choose share target").get())); - - startAndroidActivityForResult (chooserIntent, 1003, - [weakRef = WeakReference { this }] (int /*requestCode*/, - int resultCode, - LocalRef /*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; + File file; }; ContentUriElements getContentUriElements (JNIEnv* env, const LocalRef& uri) const { - jassert (prepareFilesThread != nullptr); + const auto fullUri = juceString ((jstring) env->CallObjectMethod (uri.get(), AndroidUri.toString)); - if (prepareFilesThread == nullptr) - return {}; + const auto filename = fullUri.fromLastOccurrenceOf ("/", false, true); - auto fullUri = juceString ((jstring) env->CallObjectMethod (uri.get(), AndroidUri.toString)); + const auto iter = fileForUri.find (fullUri); + const auto info = iter != fileForUri.end() ? iter->second : File{}; - auto index = fullUri.fromFirstOccurrenceOf (uriBase, false, false) - .upToFirstOccurrenceOf ("/", false, true); - - auto filename = fullUri.fromLastOccurrenceOf ("/", false, true); - - return { index, filename, prepareFilesThread->getFilePaths()[index.getIntValue()] }; + return { filename, info }; } static StringArray getSupportedColumns() @@ -726,31 +577,32 @@ private: return StringArray ("_display_name", "_size"); } - jobject getAssetFileDescriptor (JNIEnv* env, const LocalRef& contentProvider, - const String& filepath) + jobject getAssetFileDescriptor (JNIEnv* env, const LocalRef& contentProvider, const File& filepath) { - // This function can be called from multiple threads. + if (nonAssetFilePathsPendingShare.find (filepath) == nonAssetFilePathsPendingShare.end()) { - const ScopedLock sl (nonAssetFileOpenLock); - - if (! nonAssetFilePathsPendingShare.contains (filepath)) + const auto onCloseCallback = [filepath] { - nonAssetFilePathsPendingShare.add (filepath); - ++nonAssetFilesPendingShare; + ContentSharerGlobalImpl::getInstance().nonAssetFilePathsPendingShare.erase (filepath); + }; - nonAssetFileObservers.add (new AndroidContentSharerFileObserver (*this, env, - contentProvider, - filepath)); - } + auto observer = rawToUniquePtr (new AndroidContentSharerFileObserver (env, + contentProvider, + filepath, + onCloseCallback)); + + nonAssetFilePathsPendingShare.emplace (filepath, std::move (observer)); } - auto javaFile = LocalRef (env->NewObject (JavaFile, JavaFile.constructor, - javaString (filepath).get())); + const LocalRef javaFile (env->NewObject (JavaFile, + JavaFile.constructor, + javaString (filepath.getFullPathName()).get())); constexpr int modeReadOnly = 268435456; - auto parcelFileDescriptor = LocalRef (env->CallStaticObjectMethod (ParcelFileDescriptor, - ParcelFileDescriptor.open, - javaFile.get(), modeReadOnly)); + LocalRef parcelFileDescriptor (env->CallStaticObjectMethod (ParcelFileDescriptor, + ParcelFileDescriptor.open, + javaFile.get(), + modeReadOnly)); if (jniCheckHasExceptionOccurredAndClear()) { @@ -765,7 +617,8 @@ private: assetFileDescriptors.add (GlobalRef (LocalRef (env->NewObject (AssetFileDescriptor, AssetFileDescriptor.constructor, parcelFileDescriptor.get(), - startOffset, unknownLength)))); + startOffset, + unknownLength)))); return assetFileDescriptors.getReference (assetFileDescriptors.size() - 1).get(); } @@ -786,7 +639,7 @@ private: return result; } - String getCommonMimeType (const StringArray& mimeTypes) + static String getCommonMimeType (const StringArray& mimeTypes) { if (mimeTypes.isEmpty()) return "*/*"; @@ -812,72 +665,282 @@ private: return lookForCommonGroup ? commonMime + "*" : commonMime; } - ContentSharer& owner; - String packageName; - String uriBase; - - std::unique_ptr prepareFilesThread; - - bool succeeded = false; - String errorDescription; - - bool sharingActivityDidFinish = false; - - OwnedArray cursors; - + CriticalSection mutex; Array assetFileDescriptors; - - CriticalSection nonAssetFileOpenLock; - StringArray nonAssetFilePathsPendingShare; - Atomic nonAssetFilesPendingShare { 0 }; - OwnedArray nonAssetFileObservers; - - WeakReference::Master masterReference; - friend class WeakReference; - - //============================================================================== - #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 (static_cast (contentProvider)), - LocalRef (static_cast (uri)), - LocalRef (static_cast (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 (static_cast (contentProvider)), - LocalRef (static_cast (uri)), - LocalRef (static_cast (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 (static_cast (uri)), - LocalRef (static_cast (mimeTypeFilter))); - - return nullptr; - } + std::map> nonAssetFilePathsPendingShare; + std::set> cursors; + std::map fileForUri; + std::map> callbackForRequest; }; -//============================================================================== -ContentSharer::Pimpl* ContentSharer::createPimpl() +#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 : private AsyncUpdater { - return new ContentSharerNativeImpl (*this); +public: + AndroidContentSharerPrepareFilesTask (const Array& fileUrls, + std::function&, 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& 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& 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 extensions; + + for (const auto& filepath : filePaths) + { + const auto filename = filepath.fromLastOccurrenceOf ("/", false, true); + extensions.push_back (filename.fromLastOccurrenceOf (".", false, true)); + } + + std::set mimes; + + if (std::none_of (extensions.begin(), extensions.end(), [] (const String& s) { return s.isEmpty(); })) + for (const auto& extension : extensions) + for (const auto& mime : 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 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 assetFd (env->CallObjectMethod (resources, + AndroidResources.openRawResourceFd, + fileId)); + + StreamCloser inputStream (LocalRef (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 (env->NewObject (JavaFileOutputStream, + JavaFileOutputStream.constructor, + javaString (tempFile.getFullPathName()).get()))); + + if (jniCheckHasExceptionOccurredAndClear()) + { + // Failed to open file stream for temporary file + jassertfalse; + return {}; + } + + LocalRef 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 infoForUri; + StringArray mimeTypes; + std::function&, 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 task; +}; + +auto detail::ScopedContentSharerInterface::shareFiles (const Array& urls, Component*) -> std::unique_ptr +{ + class NativeScopedContentSharerInterface : public detail::ScopedContentSharerInterface + { + public: + explicit NativeScopedContentSharerInterface (Array 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& 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 files; + std::optional task; + std::unique_ptr launcher; + }; + + return std::make_unique (std::move (urls)); +} + +auto detail::ScopedContentSharerInterface::shareText (const String& text, Component*) -> std::unique_ptr +{ + class NativeScopedContentSharerInterface : 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 launcher; + }; + + return std::make_unique (std::move (text)); } } // namespace juce diff --git a/modules/juce_gui_basics/native/juce_ios_ContentSharer.cpp b/modules/juce_gui_basics/native/juce_ios_ContentSharer.cpp index 04203b55b8..2d11527fa4 100644 --- a/modules/juce_gui_basics/native/juce_ios_ContentSharer.cpp +++ b/modules/juce_gui_basics/native/juce_ios_ContentSharer.cpp @@ -26,80 +26,23 @@ namespace juce { -class ContentSharer::ContentSharerNativeImpl : public ContentSharer::Pimpl, - private Component +class NativeScopedContentSharerInterface : public detail::ScopedContentSharerInterface, + public detail::NativeModalWrapperComponent { public: - ContentSharerNativeImpl (ContentSharer& cs) - : owner (cs) + NativeScopedContentSharerInterface (Component* parentIn, NSUniquePtr itemsIn) + : parent (parentIn), items (std::move (itemsIn)) {} + + void runAsync (std::function callback) override { - static PopoverDelegateClass cls; - popoverDelegate.reset ([cls.createInstance() init]); - } - - ~ContentSharerNativeImpl() override - { - exitModalState (0); - } - - void shareFiles (const Array& files) override - { - auto urls = [NSMutableArray arrayWithCapacity: (NSUInteger) files.size()]; - - for (const auto& f : files) - { - NSString* nativeFilePath = nil; - - if (f.isLocalFile()) - { - nativeFilePath = juceStringToNS (f.getLocalFile().getFullPathName()); - } - else - { - auto filePath = f.toString (false); - - auto* fileDirectory = filePath.contains ("/") - ? juceStringToNS (filePath.upToLastOccurrenceOf ("/", false, false)) - : [NSString string]; - - auto fileName = juceStringToNS (filePath.fromLastOccurrenceOf ("/", false, false) - .upToLastOccurrenceOf (".", false, false)); - - auto fileExt = juceStringToNS (filePath.fromLastOccurrenceOf (".", false, false)); - - if ([fileDirectory length] == NSUInteger (0)) - nativeFilePath = [[NSBundle mainBundle] pathForResource: fileName - ofType: fileExt]; - else - nativeFilePath = [[NSBundle mainBundle] pathForResource: fileName - ofType: fileExt - inDirectory: fileDirectory]; - } - - if (nativeFilePath != nil) - [urls addObject: [NSURL fileURLWithPath: nativeFilePath]]; - } - - share (urls); - } - - void shareText (const String& text) override - { - auto array = [NSArray arrayWithObject: juceStringToNS (text)]; - share (array); - } - -private: - void share (NSArray* items) - { - if ([items count] == 0) + if ([items.get() count] == 0) { jassertfalse; - owner.sharingFinished (false, "No valid items found for sharing."); + NullCheckedInvocation::invoke (callback, false, "No valid items found for sharing."); return; } - controller.reset ([[UIActivityViewController alloc] initWithActivityItems: items + controller.reset ([[UIActivityViewController alloc] initWithActivityItems: items.get() applicationActivities: nil]); controller.get().excludedActivityTypes = nil; @@ -107,98 +50,76 @@ private: controller.get().completionWithItemsHandler = ^([[maybe_unused]] UIActivityType type, BOOL completed, [[maybe_unused]] NSArray* returnedItems, NSError* error) { - succeeded = completed; - - if (error != nil) - errorDescription = nsStringToJuce ([error localizedDescription]); - + const auto errorDescription = error != nil ? nsStringToJuce ([error localizedDescription]) + : String(); exitModalState (0); + + NullCheckedInvocation::invoke (callback, completed && errorDescription.isEmpty(), errorDescription); }; - controller.get().modalTransitionStyle = UIModalTransitionStyleCoverVertical; + displayNativeWindowModally (parent); - auto bounds = Desktop::getInstance().getDisplays().getPrimaryDisplay()->userArea; - setBounds (bounds); - - setAlwaysOnTop (true); - setVisible (true); - addToDesktop (0); - - enterModalState (true, - ModalCallbackFunction::create ([this] (int) - { - owner.sharingFinished (succeeded, errorDescription); - }), - false); + enterModalState (true, nullptr, false); } - static bool isIPad() + void close() override { - return [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad; + [controller.get() dismissViewControllerAnimated: YES completion: nil]; } - //============================================================================== - void parentHierarchyChanged() override - { - auto* newPeer = dynamic_cast (getPeer()); +private: + UIViewController* getViewController() const override { return controller.get(); } - if (peer != newPeer) - { - peer = newPeer; - - if (isIPad()) - { - controller.get().preferredContentSize = peer->view.frame.size; - - auto screenBounds = [UIScreen mainScreen].bounds; - - auto* popoverController = controller.get().popoverPresentationController; - popoverController.sourceView = peer->view; - popoverController.sourceRect = CGRectMake (0.f, screenBounds.size.height - 10.f, screenBounds.size.width, 10.f); - popoverController.canOverlapSourceViewRect = YES; - popoverController.delegate = popoverDelegate.get(); - } - - if (auto* parentController = peer->controller) - [parentController showViewController: controller.get() sender: parentController]; - } - } - - //============================================================================== - struct PopoverDelegateClass : public ObjCClass> - { - PopoverDelegateClass() : ObjCClass> ("PopoverDelegateClass_") - { - addMethod (@selector (popoverPresentationController:willRepositionPopoverToRect:inView:), willRepositionPopover); - - registerClass(); - } - - //============================================================================== - static void willRepositionPopover (id, SEL, UIPopoverPresentationController*, CGRect* rect, UIView*) - { - auto screenBounds = [UIScreen mainScreen].bounds; - - rect->origin.x = 0.f; - rect->origin.y = screenBounds.size.height - 10.f; - rect->size.width = screenBounds.size.width; - rect->size.height = 10.f; - } - }; - - ContentSharer& owner; - UIViewComponentPeer* peer = nullptr; + Component* parent = nullptr; NSUniquePtr controller; - NSUniquePtr> popoverDelegate; - - bool succeeded = false; - String errorDescription; + NSUniquePtr items; }; -//============================================================================== -ContentSharer::Pimpl* ContentSharer::createPimpl() +auto detail::ScopedContentSharerInterface::shareFiles (const Array& files, Component* parent) -> std::unique_ptr { - return new ContentSharerNativeImpl (*this); + NSUniquePtr urls ([[NSMutableArray arrayWithCapacity: (NSUInteger) files.size()] retain]); + + for (const auto& f : files) + { + NSString* nativeFilePath = nil; + + if (f.isLocalFile()) + { + nativeFilePath = juceStringToNS (f.getLocalFile().getFullPathName()); + } + else + { + auto filePath = f.toString (false); + + auto* fileDirectory = filePath.contains ("/") + ? juceStringToNS (filePath.upToLastOccurrenceOf ("/", false, false)) + : [NSString string]; + + auto fileName = juceStringToNS (filePath.fromLastOccurrenceOf ("/", false, false) + .upToLastOccurrenceOf (".", false, false)); + + auto fileExt = juceStringToNS (filePath.fromLastOccurrenceOf (".", false, false)); + + if ([fileDirectory length] == NSUInteger (0)) + nativeFilePath = [[NSBundle mainBundle] pathForResource: fileName + ofType: fileExt]; + else + nativeFilePath = [[NSBundle mainBundle] pathForResource: fileName + ofType: fileExt + inDirectory: fileDirectory]; + } + + if (nativeFilePath != nil) + [urls.get() addObject: [NSURL fileURLWithPath: nativeFilePath]]; + } + + return std::make_unique (parent, std::move (urls)); +} + +auto detail::ScopedContentSharerInterface::shareText (const String& text, Component* parent) -> std::unique_ptr +{ + NSUniquePtr array ([[NSArray arrayWithObject: juceStringToNS (text)] retain]); + return std::make_unique (parent, std::move (array)); } } // namespace juce diff --git a/modules/juce_gui_basics/native/juce_ios_FileChooser.mm b/modules/juce_gui_basics/native/juce_ios_FileChooser.mm index cd97759d46..2d6661abea 100644 --- a/modules/juce_gui_basics/native/juce_ios_FileChooser.mm +++ b/modules/juce_gui_basics/native/juce_ios_FileChooser.mm @@ -39,8 +39,9 @@ namespace juce #define JUCE_DEPRECATION_IGNORED 1 #endif +//============================================================================== class FileChooser::Native : public FileChooser::Pimpl, - public Component, + public detail::NativeModalWrapperComponent, public AsyncUpdater, public std::enable_shared_from_this { @@ -56,11 +57,6 @@ public: return result; } - ~Native() override - { - exitModalState (0); - } - void launch() override { jassert (shared_from_this() != nullptr); @@ -90,24 +86,6 @@ public: #endif } - void parentHierarchyChanged() override - { - auto* newPeer = dynamic_cast (getPeer()); - - if (peer != newPeer) - { - peer = newPeer; - - if (peer != nullptr) - { - if (auto* parentController = peer->controller) - [parentController showViewController: controller.get() sender: parentController]; - - peer->toFront (false); - } - } - } - void handleAsyncUpdate() override { pickerWasCancelled(); @@ -189,6 +167,8 @@ public: } private: + UIViewController* getViewController() const override { return controller.get(); } + Native (FileChooser& fileChooser, int flags) : owner (fileChooser) { @@ -241,40 +221,9 @@ private: [controller.get() setAllowsMultipleSelection: (flags & FileBrowserComponent::canSelectMultipleItems) != 0]; } - [controller.get() setDelegate: delegate.get()]; - [controller.get() setModalTransitionStyle: UIModalTransitionStyleCrossDissolve]; - setOpaque (false); - - if (fileChooser.parent != nullptr) - { - [controller.get() setModalPresentationStyle: UIModalPresentationFullScreen]; - - auto chooserBounds = fileChooser.parent->getBounds(); - setBounds (chooserBounds); - - setAlwaysOnTop (true); - fileChooser.parent->addAndMakeVisible (this); - } - else - { - if (SystemStats::isRunningInAppExtensionSandbox()) - { - // Opening a native top-level window in an AUv3 is not allowed (sandboxing). You need to specify a - // parent component (for example your editor) to parent the native file chooser window. To do this - // specify a parent component in the FileChooser's constructor! - jassertfalse; - return; - } - - auto chooserBounds = Desktop::getInstance().getDisplays().getPrimaryDisplay()->userArea; - setBounds (chooserBounds); - - setAlwaysOnTop (true); - setVisible (true); - addToDesktop (0); - } + displayNativeWindowModally (fileChooser.parent); } void passResultsToInitiator (Array urls) @@ -363,7 +312,6 @@ private: FileChooser& owner; NSUniquePtr> delegate; NSUniquePtr controller; - UIViewComponentPeer* peer = nullptr; //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Native) diff --git a/modules/juce_gui_basics/native/juce_ios_NativeModalWrapperComponent.h b/modules/juce_gui_basics/native/juce_ios_NativeModalWrapperComponent.h new file mode 100644 index 0000000000..183aaa01ac --- /dev/null +++ b/modules/juce_gui_basics/native/juce_ios_NativeModalWrapperComponent.h @@ -0,0 +1,132 @@ +/* + ============================================================================== + + 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::detail +{ + +/** + Sets up a native control to be hosted on top of a JUCE component. +*/ +class NativeModalWrapperComponent : public Component +{ +public: + void parentHierarchyChanged() final + { + auto* newPeer = dynamic_cast (getPeer()); + + if (std::exchange (peer, newPeer) == newPeer) + return; + + if (peer == nullptr) + return; + + if (isIPad()) + { + getViewController().preferredContentSize = peer->view.frame.size; + + if (auto* popoverController = getViewController().popoverPresentationController) + { + popoverController.sourceView = peer->view; + popoverController.sourceRect = CGRectMake (0.f, getHeight() - 10.f, getWidth(), 10.f); + popoverController.canOverlapSourceViewRect = YES; + popoverController.delegate = popoverDelegate.get(); + } + } + + if (auto* parentController = peer->controller) + [parentController showViewController: getViewController() sender: parentController]; + + peer->toFront (false); + } + + void displayNativeWindowModally (Component* parent) + { + setOpaque (false); + + if (parent != nullptr) + { + [getViewController() setModalPresentationStyle: UIModalPresentationPageSheet]; + + setBounds (parent->getLocalBounds()); + + setAlwaysOnTop (true); + parent->addAndMakeVisible (this); + } + else + { + if (SystemStats::isRunningInAppExtensionSandbox()) + { + // Opening a native top-level window in an AUv3 is not allowed (sandboxing). You need to specify a + // parent component (for example your editor) to parent the native file chooser window. To do this + // specify a parent component in the FileChooser's constructor! + jassertfalse; + return; + } + + auto chooserBounds = Desktop::getInstance().getDisplays().getPrimaryDisplay()->userArea; + setBounds (chooserBounds); + + setAlwaysOnTop (true); + setVisible (true); + addToDesktop (0); + } + } + +private: + virtual UIViewController* getViewController() const = 0; + + static bool isIPad() + { + return [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad; + } + + struct PopoverDelegateClass : public ObjCClass> + { + PopoverDelegateClass() + : ObjCClass ("PopoverDelegateClass_") + { + addMethod (@selector (popoverPresentationController:willRepositionPopoverToRect:inView:), [] (id, SEL, UIPopoverPresentationController*, CGRect* rect, UIView*) + { + auto screenBounds = [UIScreen mainScreen].bounds; + + rect->origin.x = 0.f; + rect->origin.y = screenBounds.size.height - 10.f; + rect->size.width = screenBounds.size.width; + rect->size.height = 10.f; + }); + + registerClass(); + } + }; + + UIViewComponentPeer* peer = nullptr; + NSUniquePtr> popoverDelegate { [] + { + static PopoverDelegateClass cls; + return cls.createInstance(); + }() }; +}; + +} // namespace juce::detail diff --git a/modules/juce_gui_basics/windows/juce_AlertWindow.cpp b/modules/juce_gui_basics/windows/juce_AlertWindow.cpp index 802bae7a3d..44e036cdce 100644 --- a/modules/juce_gui_basics/windows/juce_AlertWindow.cpp +++ b/modules/juce_gui_basics/windows/juce_AlertWindow.cpp @@ -37,7 +37,7 @@ static juce_wchar getDefaultPasswordChar() noexcept static int showAlertWindowUnmanaged (const MessageBoxOptions& opts, ModalComponentManager::Callback* cb) { - return detail::ScopedMessageBoxImpl::showUnmanaged (detail::AlertWindowHelpers::create (opts), cb); + return detail::ConcreteScopedMessageBoxImpl::showUnmanaged (detail::AlertWindowHelpers::create (opts), cb); } //============================================================================== @@ -693,7 +693,7 @@ ScopedMessageBox AlertWindow::showScopedAsync (const MessageBoxOptions& options, if (LookAndFeel::getDefaultLookAndFeel().isUsingNativeAlertWindows()) return NativeMessageBox::showScopedAsync (options, std::move (callback)); - return detail::ScopedMessageBoxImpl::show (detail::AlertWindowHelpers::create (options), std::move (callback)); + return detail::ConcreteScopedMessageBoxImpl::show (detail::AlertWindowHelpers::create (options), std::move (callback)); } //============================================================================== diff --git a/modules/juce_gui_basics/windows/juce_NativeMessageBox.cpp b/modules/juce_gui_basics/windows/juce_NativeMessageBox.cpp index f6925f6d6e..b5cf95879b 100644 --- a/modules/juce_gui_basics/windows/juce_NativeMessageBox.cpp +++ b/modules/juce_gui_basics/windows/juce_NativeMessageBox.cpp @@ -78,7 +78,7 @@ static int showNativeBoxUnmanaged (const MessageBoxOptions& opts, ResultCodeMappingMode mode) { auto implementation = makeNativeMessageBoxWithMappedResult (opts, mode); - return detail::ScopedMessageBoxImpl::showUnmanaged (std::move (implementation), cb); + return detail::ConcreteScopedMessageBoxImpl::showUnmanaged (std::move (implementation), cb); } #if JUCE_MODAL_LOOPS_PERMITTED @@ -152,7 +152,7 @@ void JUCE_CALLTYPE NativeMessageBox::showAsync (const MessageBoxOptions& options ScopedMessageBox NativeMessageBox::showScopedAsync (const MessageBoxOptions& options, std::function callback) { auto implementation = makeNativeMessageBoxWithMappedResult (options, ResultCodeMappingMode::alertWindow); - return detail::ScopedMessageBoxImpl::show (std::move (implementation), std::move (callback)); + return detail::ConcreteScopedMessageBoxImpl::show (std::move (implementation), std::move (callback)); } } // namespace juce diff --git a/modules/juce_gui_basics/windows/juce_ScopedMessageBox.h b/modules/juce_gui_basics/windows/juce_ScopedMessageBox.h index d1234bb1c1..bd2cba7b0c 100644 --- a/modules/juce_gui_basics/windows/juce_ScopedMessageBox.h +++ b/modules/juce_gui_basics/windows/juce_ScopedMessageBox.h @@ -34,6 +34,9 @@ namespace juce class ScopedMessageBox { public: + /** @internal */ + explicit ScopedMessageBox (std::shared_ptr); + /** Constructor */ ScopedMessageBox(); @@ -56,9 +59,6 @@ public: void close(); private: - friend detail::ScopedMessageBoxImpl; - explicit ScopedMessageBox (std::shared_ptr); - std::shared_ptr impl; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ScopedMessageBox)