/* ============================================================================== This file is part of the JUCE framework. Copyright (c) Raw Material Software Limited JUCE is an open source framework subject to commercial or open source licensing. By downloading, installing, or using the JUCE framework, or combining the JUCE framework with any other source code, object code, content or any other copyrightable work, you agree to the terms of the JUCE End User Licence Agreement, and all incorporated terms including the JUCE Privacy Policy and the JUCE Website Terms of Service, as applicable, which will bind you. If you do not agree to the terms of these agreements, we will not license the JUCE framework to you, and you must discontinue the installation or download process and cease use of the JUCE framework. JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ JUCE Privacy Policy: https://juce.com/juce-privacy-policy JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ Or: You may also use this code under the terms of the AGPLv3: https://www.gnu.org/licenses/agpl-3.0.en.html THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. ============================================================================== */ namespace juce { //============================================================================== // This byte-code is generated from native/java/app/com/rmsl/juce/JuceContentProviderCursor.java with min sdk version 16 // See juce_core/native/java/README.txt on how to generate this byte-code. static const uint8 javaJuceContentProviderCursor[] = {31,139,8,8,191,114,161,94,0,3,106,97,118,97,74,117,99,101,67,111,110,116,101,110,116,80,114,111,118,105,100,101,114,67,117, 114,115,111,114,46,100,101,120,0,117,147,177,111,211,64,20,198,223,157,157,148,150,54,164,192,208,14,64,144,16,18,67,235,138,2, 75,40,162,10,44,150,65,149,2,25,218,233,176,173,198,37,241,69,182,19,121,96,160,21,136,37,19,98,234,80,85,149,152,88,24,248, 3,24,146,63,130,141,137,129,13,169,99,7,190,203,157,33,18,194,210,207,247,222,229,189,239,157,206,95,130,48,159,91,91,191,75,227, 60,200,143,134,239,247,151,62,189,43,175,127,249,246,235,241,215,241,112,231,231,193,237,135,22,81,143,136,242,214,157,139, 100,158,99,78,84,37,189,95,2,159,129,13,70,128,129,83,179,127,102,242,27,120,157,129,71,224,16,156,128,143,96,12,126,128,69,232, 93,6,75,224,10,184,14,238,129,13,224,130,16,188,4,3,174,245,44,51,79,205,152,53,171,101,206,86,54,241,27,20,206,152,120,136, 248,156,137,63,32,134,12,45,76,206,166,187,148,230,28,169,125,62,201,249,159,156,209,188,201,23,77,93,241,187,122,134,38,40,225, 52,42,124,197,245,252,94,141,104,147,182,113,95,21,76,208,83,222,114,125,86,89,101,168,109,162,162,183,134,46,86,249,71,215, 158,228,54,149,239,71,113,148,61,32,230,210,85,183,239,135,13,25,103,97,156,109,37,114,16,5,97,210,232,39,169,76,86,247,196,64, 208,53,79,196,65,34,163,192,9,68,38,94,136,52,116,158,136,44,137,114,93,84,167,91,158,47,187,78,210,77,59,206,30,164,156,255, 234,213,137,181,136,183,92,178,90,174,135,192,163,75,59,158,154,225,116,68,188,235,52,33,26,239,214,169,228,119,100,26,210,121, 95,118,250,221,248,169,232,134,41,45,251,90,176,217,22,73,33,215,80,101,1,217,109,153,102,52,171,222,207,228,115,52,218,89, 59,74,233,38,191,48,63,83,217,88,161,85,194,178,141,139,224,184,28,190,255,218,30,113,126,192,201,98,223,249,130,185,27,54,181, 22,222,227,83,254,43,60,49,50,235,180,15,11,47,150,167,252,200,106,186,95,121,146,85,255,122,134,215,180,190,242,169,101,106, 212,119,165,154,238,157,124,243,170,142,213,255,224,55,143,234,50,200,64,3,0,0,0,0}; // This byte-code is generated from native/java/app/com/rmsl/juce/JuceContentProviderFileObserver.java with min sdk version 16 // See juce_core/native/java/README.txt on how to generate this byte-code. static const uint8 javaJuceContentProviderFileObserver[] = {31,139,8,8,194,122,161,94,0,3,106,97,118,97,74,117,99,101,67,111,110,116,101,110,116,80,114,111,118,105,100,101,114,70,105, 108,101,79,98,115,101,114,118,101,114,46,100,101,120,0,133,147,205,107,19,65,24,198,223,249,72,98,171,46,105,235,69,16,201,65,81, 68,221,136,10,66,84,144,250,65,194,130,197,212,32,5,15,155,100,104,182,38,187,97,119,141,241,32,126,30,196,147,23,79,246,216, 131,120,202,77,169,80,212,191,64,193,66,143,30,60,138,255,130,62,179,51,165,219,147,129,223,188,239,188,239,204,179,179,179,79, 186,106,60,93,61,123,158,54,159,255,248,112,97,210,120,124,98,237,251,177,7,109,245,115,253,225,198,159,47,243,171,135,198,130, 104,72,68,227,214,185,89,178,191,45,78,116,128,76,189,8,62,3,169,235,128,129,61,204,204,203,204,204,171,24,142,99,207,2,226, 4,124,4,159,192,6,248,5,254,130,42,250,87,193,13,224,129,91,224,14,184,11,30,129,23,224,21,120,3,222,130,53,240,158,27,125,110, 159,95,176,231,41,233,51,216,249,75,44,152,178,249,107,228,211,54,95,69,190,215,230,239,144,11,40,57,153,150,200,222,81,100, 170,166,190,47,139,68,51,185,200,237,93,8,27,191,218,66,17,138,186,54,225,230,44,195,42,209,149,194,18,238,206,201,58,250,121, 235,182,215,172,160,191,200,137,159,113,172,158,204,246,50,251,62,38,151,89,103,251,29,139,23,131,48,72,47,19,171,19,107,208, 145,198,253,142,154,143,194,84,133,233,66,28,141,130,174,138,175,7,125,117,179,157,168,120,164,226,211,43,254,200,167,131,158, 31,118,227,40,232,186,81,226,230,219,53,114,189,78,52,112,227,65,210,119,87,32,229,254,71,175,70,179,158,150,116,251,126,184, 236,54,211,56,8,151,107,196,90,36,90,117,143,100,171,97,70,175,142,2,134,195,29,35,213,236,249,241,110,161,107,35,148,169,160, 178,32,123,81,146,210,148,30,23,163,219,137,34,57,240,147,123,84,138,66,179,76,14,253,180,71,50,237,5,9,29,21,229,185,153,146, 115,233,20,157,228,206,92,201,89,194,21,113,70,156,61,125,34,191,113,246,12,223,143,253,198,101,237,183,223,133,229,226,182,103, 121,206,183,34,231,93,153,243,111,129,118,60,92,164,29,31,179,138,217,175,189,204,202,102,141,246,24,175,24,125,237,111,97, 215,104,15,80,197,236,205,252,81,54,185,254,255,252,3,243,31,208,130,120,3,0,0,0,0}; //============================================================================== #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ FIELD (authority, "authority", "Ljava/lang/String;") DECLARE_JNI_CLASS (AndroidProviderInfo, "android/content/pm/ProviderInfo") #undef JNI_CLASS_MEMBERS #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (constructor, "", "(Landroid/os/ParcelFileDescriptor;JJ)V") \ METHOD (createInputStream, "createInputStream", "()Ljava/io/FileInputStream;") \ METHOD (getLength, "getLength", "()J") DECLARE_JNI_CLASS (AssetFileDescriptor, "android/content/res/AssetFileDescriptor") #undef JNI_CLASS_MEMBERS #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (close, "close", "()V") DECLARE_JNI_CLASS (JavaCloseable, "java/io/Closeable") #undef JNI_CLASS_MEMBERS #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ STATICMETHOD (open, "open", "(Ljava/io/File;I)Landroid/os/ParcelFileDescriptor;") DECLARE_JNI_CLASS (ParcelFileDescriptor, "android/os/ParcelFileDescriptor") #undef JNI_CLASS_MEMBERS //============================================================================== class AndroidContentSharerCursor { public: AndroidContentSharerCursor (JNIEnv* env, const LocalRef& contentProvider, const LocalRef& resultColumns, std::function onCloseIn) : onClose (std::move (onCloseIn)), cursor (GlobalRef (LocalRef (env->NewObject (JuceContentProviderCursor, JuceContentProviderCursor.constructor, reinterpret_cast (this), resultColumns.get())))) { // the content provider must be created first jassert (contentProvider.get() != nullptr); } jobject getNativeCursor() const { return cursor.get(); } void addRow (LocalRef& values) { auto* env = getEnv(); env->CallVoidMethod (cursor.get(), JuceContentProviderCursor.addRow, values.get()); } private: static void cursorClosed (JNIEnv*, AndroidContentSharerCursor& t) { MessageManager::callAsync ([&t] { NullCheckedInvocation::invoke (t.onClose, t); }); } std::function onClose; GlobalRef cursor; //============================================================================== #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (addRow, "addRow", "([Ljava/lang/Object;)V") \ METHOD (constructor, "", "(J[Ljava/lang/String;)V") \ CALLBACK (generatedCallback<&AndroidContentSharerCursor::cursorClosed>, "contentSharerCursorClosed", "(J)V") \ DECLARE_JNI_CLASS_WITH_BYTECODE (JuceContentProviderCursor, "com/rmsl/juce/JuceContentProviderCursor", 16, javaJuceContentProviderCursor) #undef JNI_CLASS_MEMBERS }; //============================================================================== class AndroidContentSharerFileObserver { public: AndroidContentSharerFileObserver (JNIEnv* env, const LocalRef& contentProvider, 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.getFullPathName()).get(), open | access | closeWrite | closeNoWrite)))) { // the content provider must be created first jassert (contentProvider.get() != nullptr); env->CallVoidMethod (fileObserver, JuceContentProviderFileObserver.startWatching); } void onFileEvent (int event, const LocalRef&) { if (event == open) { ++numOpenedHandles; } else if (event == access) { fileWasRead = true; } else if (event == closeNoWrite || event == closeWrite) { --numOpenedHandles; // numOpenedHandles may get negative if we don't receive open handle event. if (fileWasRead && numOpenedHandles <= 0) { MessageManager::callAsync ([fileObserver = fileObserver, onClose = onClose] { getEnv()->CallVoidMethod (fileObserver, JuceContentProviderFileObserver.stopWatching); NullCheckedInvocation::invoke (onClose); }); } } } private: static constexpr int open = 32; static constexpr int access = 1; static constexpr int closeWrite = 8; static constexpr int closeNoWrite = 16; std::function onClose; bool fileWasRead = false; int numOpenedHandles = 0; File filepath; GlobalRef fileObserver; //============================================================================== #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (constructor, "", "(JLjava/lang/String;I)V") \ METHOD (startWatching, "startWatching", "()V") \ METHOD (stopWatching, "stopWatching", "()V") \ CALLBACK (generatedCallback<&AndroidContentSharerFileObserver::onFileEventCallback>, "contentSharerFileObserverEvent", "(JILjava/lang/String;)V") \ DECLARE_JNI_CLASS_WITH_BYTECODE (JuceContentProviderFileObserver, "com/rmsl/juce/JuceContentProviderFileObserver", 16, javaJuceContentProviderFileObserver) #undef JNI_CLASS_MEMBERS static void onFileEventCallback (JNIEnv*, AndroidContentSharerFileObserver& t, jint event, jstring path) { t.onFileEvent (event, LocalRef (path)); } }; //============================================================================== class ContentSharerGlobalImpl { public: static ContentSharerGlobalImpl& getInstance() { static ContentSharerGlobalImpl result; return result; } 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) { // This function should be called from the main thread, but must not race with singleton // access from other threads. const ScopedLock lock { mutex }; if (! isContentSharingEnabled()) { // You need to enable "Content Sharing" in Projucer's Android exporter. jassertfalse; NullCheckedInvocation::invoke (callback, false); return {}; } auto* env = getEnv(); fileForUri.insert (fileForUriIn.begin(), fileForUriIn.end()); 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()); const auto permissions = [&] { constexpr int grantReadUriPermission = 1; constexpr int grantPrefixUriPermission = 128; return grantReadUriPermission | grantPrefixUriPermission; }; env->CallObjectMethod (intent, AndroidIntent.setFlags, permissions); 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, javaString ("android.intent.extra.TEXT").get(), javaString (text).get()); env->CallObjectMethod (intent, AndroidIntent.setType, javaString ("text/plain").get()); return doIntent (intent, callback); } static void onBroadcastResultReceive (JNIEnv*, jobject, int requestCode) { getInstance().sharingFinished (requestCode, true); } static jobject JNICALL contentSharerQuery (JNIEnv*, jobject contentProvider, jobject uri, jobjectArray projection) { return getInstance().query (LocalRef (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 (addLocalRefOwner (uri), addLocalRefOwner (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) { // This function can be called from multiple threads. const ScopedLock lock { mutex }; auto* env = getEnv(); auto uriElements = getContentUriElements (env, uri); if (uriElements.file == File()) return nullptr; return getAssetFileDescriptor (env, contentProvider, uriElements.file); } jobject query (const LocalRef& 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(); StringArray resultColumns; for (const auto& col : supportedColumns) { if (requestedColumns.contains (col)) resultColumns.add (col); } // Unsupported columns were queried, file sharing may fail. if (resultColumns.isEmpty()) return nullptr; auto resultJavaColumns = juceStringArrayToJava (resultColumns); auto* env = getEnv(); const auto uriElements = getContentUriElements (env, uri); const auto callback = [info = uriElements.file] (auto& ref) { auto& pimplCursors = ContentSharerGlobalImpl::getInstance().cursors; const auto iter = std::lower_bound (pimplCursors.begin(), pimplCursors.end(), &ref, [] (const auto& managed, const auto* ptr) { return managed.get() == ptr; }); if (iter != pimplCursors.end() && iter->get() == &ref) pimplCursors.erase (iter); }; auto [iter, inserted] = cursors.emplace (new AndroidContentSharerCursor (env, contentProvider, resultJavaColumns, callback)); if (uriElements.file == File()) return (*iter)->getNativeCursor(); LocalRef values (env->NewObjectArray ((jsize) resultColumns.size(), JavaObject, nullptr)); for (int i = 0; i < resultColumns.size(); ++i) { if (resultColumns.getReference (i) == "_display_name") { env->SetObjectArrayElement (values, i, javaString (uriElements.filename).get()); } else if (resultColumns.getReference (i) == "_size") { LocalRef javaFile (env->NewObject (JavaFile, JavaFile.constructor, javaString (uriElements.file.getFullPathName()).get())); jlong fileLength = env->CallLongMethod (javaFile, JavaFile.length); env->SetObjectArrayElement (values, i, env->NewObject (JavaLong, JavaLong.constructor, fileLength)); } } (*iter)->addRow (values); return (*iter)->getNativeCursor(); } jobjectArray getStreamTypes (const LocalRef& 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); if (extension.isEmpty()) return nullptr; return juceStringArrayToJava (filterMimeTypes (detail::MimeTypeTable::getMimeTypesForFileExtension (extension), juceString (mimeTypeFilter.get()))).release(); } std::unique_ptr doIntent (const LocalRef& intent, std::function callback) { static std::atomic lastRequest = 1003; const auto requestCode = lastRequest++; callbackForRequest.emplace (requestCode, callback); const auto chooser = makeChooser (intent, requestCode); 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); } bool isContentSharingEnabled() const { auto* env = getEnv(); LocalRef packageManager (env->CallObjectMethod (getAppContext().get(), AndroidContext.getPackageManager)); constexpr int getProviders = 8; LocalRef packageInfo (env->CallObjectMethod (packageManager, AndroidPackageManager.getPackageInfo, javaString (packageName).get(), getProviders)); LocalRef providers ((jobjectArray) env->GetObjectField (packageInfo, AndroidPackageInfo.providers)); if (providers == nullptr) return false; auto sharingContentProviderAuthority = packageName + ".sharingcontentprovider"; const int numProviders = env->GetArrayLength (providers.get()); for (int i = 0; i < numProviders; ++i) { LocalRef providerInfo (env->GetObjectArrayElement (providers, i)); LocalRef authority ((jstring) env->GetObjectField (providerInfo, AndroidProviderInfo.authority)); if (juceString (authority) == sharingContentProviderAuthority) return true; } return false; } //============================================================================== struct ContentUriElements { String filename; File file; }; ContentUriElements getContentUriElements (JNIEnv* env, const LocalRef& uri) const { const auto fullUri = juceString ((jstring) env->CallObjectMethod (uri.get(), AndroidUri.toString)); const auto filename = fullUri.fromLastOccurrenceOf ("/", false, true); const auto iter = fileForUri.find (fullUri); const auto info = iter != fileForUri.end() ? iter->second : File{}; return { filename, info }; } static StringArray getSupportedColumns() { return StringArray ("_display_name", "_size"); } jobject getAssetFileDescriptor (JNIEnv* env, const LocalRef& contentProvider, const File& filepath) { if (nonAssetFilePathsPendingShare.find (filepath) == nonAssetFilePathsPendingShare.end()) { const auto onCloseCallback = [filepath] { ContentSharerGlobalImpl::getInstance().nonAssetFilePathsPendingShare.erase (filepath); }; auto observer = rawToUniquePtr (new AndroidContentSharerFileObserver (env, contentProvider, filepath, onCloseCallback)); nonAssetFilePathsPendingShare.emplace (filepath, std::move (observer)); } const LocalRef javaFile (env->NewObject (JavaFile, JavaFile.constructor, javaString (filepath.getFullPathName()).get())); constexpr int modeReadOnly = 268435456; LocalRef parcelFileDescriptor (env->CallStaticObjectMethod (ParcelFileDescriptor, ParcelFileDescriptor.open, javaFile.get(), modeReadOnly)); if (jniCheckHasExceptionOccurredAndClear()) { // Failed to create file descriptor. Have you provided a valid file path/resource name? jassertfalse; return nullptr; } jlong startOffset = 0; jlong unknownLength = -1; assetFileDescriptors.add (GlobalRef (LocalRef (env->NewObject (AssetFileDescriptor, AssetFileDescriptor.constructor, parcelFileDescriptor.get(), startOffset, unknownLength)))); return assetFileDescriptors.getReference (assetFileDescriptors.size() - 1).get(); } StringArray filterMimeTypes (const StringArray& mimeTypes, const String& filter) { String filterToUse (filter.removeCharacters ("*")); if (filterToUse.isEmpty() || filterToUse == "/") return mimeTypes; StringArray result; for (const auto& type : mimeTypes) if (String (type).contains (filterToUse)) result.add (type); return result; } static String getCommonMimeType (const StringArray& mimeTypes) { if (mimeTypes.isEmpty()) return "*/*"; auto commonMime = mimeTypes[0]; bool lookForCommonGroup = false; for (int i = 1; i < mimeTypes.size(); ++i) { if (mimeTypes[i] == commonMime) continue; if (! lookForCommonGroup) { lookForCommonGroup = true; commonMime = commonMime.upToFirstOccurrenceOf ("/", true, false); } if (! mimeTypes[i].startsWith (commonMime)) return "*/*"; } return lookForCommonGroup ? commonMime + "*" : commonMime; } CriticalSection mutex; Array assetFileDescriptors; std::map> nonAssetFilePathsPendingShare; std::set> cursors; std::map fileForUri; std::map> callbackForRequest; }; #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ CALLBACK (ContentSharerGlobalImpl::contentSharerQuery, "contentSharerQuery", "(Landroid/net/Uri;[Ljava/lang/String;)Landroid/database/Cursor;") \ CALLBACK (ContentSharerGlobalImpl::contentSharerOpenFile, "contentSharerOpenFile", "(Landroid/net/Uri;Ljava/lang/String;)Landroid/content/res/AssetFileDescriptor;") \ CALLBACK (ContentSharerGlobalImpl::contentSharerGetStreamTypes, "contentSharerGetStreamTypes", "(Landroid/net/Uri;Ljava/lang/String;)[Ljava/lang/String;") \ DECLARE_JNI_CLASS (JuceSharingContentProvider, "com/rmsl/juce/JuceSharingContentProvider") #undef JNI_CLASS_MEMBERS #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ CALLBACK (ContentSharerGlobalImpl::onBroadcastResultReceive, "onBroadcastResultNative", "(I)V") DECLARE_JNI_CLASS (AndroidReceiver, "com/rmsl/juce/Receiver") #undef JNI_CLASS_MEMBERS //============================================================================== class AndroidContentSharerPrepareFilesTask final : private AsyncUpdater { public: AndroidContentSharerPrepareFilesTask (const Array& 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 : detail::MimeTypeTable::getMimeTypesForFileExtension (extension)) mimes.insert (mime); for (const auto& mime : mimes) mimeTypes.add (mime); for (auto it = filePaths.begin(); it != filePaths.end(); ++it) { const auto filename = it->fromLastOccurrenceOf ("/", false, true); const auto contentString = uriBase + String (std::distance (filePaths.begin(), it)) + "/" + filename; infoForUri.emplace (contentString, *it); } } URL copyAssetFileToTemporaryFile (JNIEnv* env, const String& filename) { LocalRef 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 final : 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 final : public detail::ScopedContentSharerInterface { public: explicit NativeScopedContentSharerInterface (String t) : text (std::move (t)) {} void runAsync (ContentSharer::Callback callback) override { launcher = ContentSharerGlobalImpl::getInstance().shareText (text, [callback] (bool success) { callback (success, {}); }); } void close() override { // dismiss() doesn't close the sharesheet, and there doesn't seem to be any alternative // Maybe this will work in the future... launcher.reset(); } private: String text; std::unique_ptr launcher; }; return std::make_unique (std::move (text)); } } // namespace juce