/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2020 - 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 6 End-User License Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). End User License Agreement: www.juce.com/juce-6-licence Privacy Policy: www.juce.com/juce-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ namespace juce { //============================================================================== // This byte-code is generated from native/java/app/com/rmsl/juce/JuceContentProviderCursor.java with min sdk version 16 // See juce_core/native/java/README.txt on how to generate this byte-code. static const uint8 javaJuceContentProviderCursor[] = {31,139,8,8,191,114,161,94,0,3,106,97,118,97,74,117,99,101,67,111,110,116,101,110,116,80,114,111,118,105,100,101,114,67,117, 114,115,111,114,46,100,101,120,0,117,147,177,111,211,64,20,198,223,157,157,148,150,54,164,192,208,14,64,144,16,18,67,235,138,2, 75,40,162,10,44,150,65,149,2,25,218,233,176,173,198,37,241,69,182,19,121,96,160,21,136,37,19,98,234,80,85,149,152,88,24,248, 3,24,146,63,130,141,137,129,13,169,99,7,190,203,157,33,18,194,210,207,247,222,229,189,239,157,206,95,130,48,159,91,91,191,75,227, 60,200,143,134,239,247,151,62,189,43,175,127,249,246,235,241,215,241,112,231,231,193,237,135,22,81,143,136,242,214,157,139, 100,158,99,78,84,37,189,95,2,159,129,13,70,128,129,83,179,127,102,242,27,120,157,129,71,224,16,156,128,143,96,12,126,128,69,232, 93,6,75,224,10,184,14,238,129,13,224,130,16,188,4,3,174,245,44,51,79,205,152,53,171,101,206,86,54,241,27,20,206,152,120,136, 248,156,137,63,32,134,12,45,76,206,166,187,148,230,28,169,125,62,201,249,159,156,209,188,201,23,77,93,241,187,122,134,38,40,225, 52,42,124,197,245,252,94,141,104,147,182,113,95,21,76,208,83,222,114,125,86,89,101,168,109,162,162,183,134,46,86,249,71,215, 158,228,54,149,239,71,113,148,61,32,230,210,85,183,239,135,13,25,103,97,156,109,37,114,16,5,97,210,232,39,169,76,86,247,196,64, 208,53,79,196,65,34,163,192,9,68,38,94,136,52,116,158,136,44,137,114,93,84,167,91,158,47,187,78,210,77,59,206,30,164,156,255, 234,213,137,181,136,183,92,178,90,174,135,192,163,75,59,158,154,225,116,68,188,235,52,33,26,239,214,169,228,119,100,26,210,121, 95,118,250,221,248,169,232,134,41,45,251,90,176,217,22,73,33,215,80,101,1,217,109,153,102,52,171,222,207,228,115,52,218,89, 59,74,233,38,191,48,63,83,217,88,161,85,194,178,141,139,224,184,28,190,255,218,30,113,126,192,201,98,223,249,130,185,27,54,181, 22,222,227,83,254,43,60,49,50,235,180,15,11,47,150,167,252,200,106,186,95,121,146,85,255,122,134,215,180,190,242,169,101,106, 212,119,165,154,238,157,124,243,170,142,213,255,224,55,143,234,50,200,64,3,0,0,0,0}; // This byte-code is generated from native/java/app/com/rmsl/juce/JuceContentProviderFileObserver.java with min sdk version 16 // See juce_core/native/java/README.txt on how to generate this byte-code. static const uint8 javaJuceContentProviderFileObserver[] = {31,139,8,8,194,122,161,94,0,3,106,97,118,97,74,117,99,101,67,111,110,116,101,110,116,80,114,111,118,105,100,101,114,70,105, 108,101,79,98,115,101,114,118,101,114,46,100,101,120,0,133,147,205,107,19,65,24,198,223,249,72,98,171,46,105,235,69,16,201,65,81, 68,221,136,10,66,84,144,250,65,194,130,197,212,32,5,15,155,100,104,182,38,187,97,119,141,241,32,126,30,196,147,23,79,246,216, 131,120,202,77,169,80,212,191,64,193,66,143,30,60,138,255,130,62,179,51,165,219,147,129,223,188,239,188,239,204,179,179,179,79, 186,106,60,93,61,123,158,54,159,255,248,112,97,210,120,124,98,237,251,177,7,109,245,115,253,225,198,159,47,243,171,135,198,130, 104,72,68,227,214,185,89,178,191,45,78,116,128,76,189,8,62,3,169,235,128,129,61,204,204,203,204,204,171,24,142,99,207,2,226, 4,124,4,159,192,6,248,5,254,130,42,250,87,193,13,224,129,91,224,14,184,11,30,129,23,224,21,120,3,222,130,53,240,158,27,125,110, 159,95,176,231,41,233,51,216,249,75,44,152,178,249,107,228,211,54,95,69,190,215,230,239,144,11,40,57,153,150,200,222,81,100, 170,166,190,47,139,68,51,185,200,237,93,8,27,191,218,66,17,138,186,54,225,230,44,195,42,209,149,194,18,238,206,201,58,250,121, 235,182,215,172,160,191,200,137,159,113,172,158,204,246,50,251,62,38,151,89,103,251,29,139,23,131,48,72,47,19,171,19,107,208, 145,198,253,142,154,143,194,84,133,233,66,28,141,130,174,138,175,7,125,117,179,157,168,120,164,226,211,43,254,200,167,131,158, 31,118,227,40,232,186,81,226,230,219,53,114,189,78,52,112,227,65,210,119,87,32,229,254,71,175,70,179,158,150,116,251,126,184, 236,54,211,56,8,151,107,196,90,36,90,117,143,100,171,97,70,175,142,2,134,195,29,35,213,236,249,241,110,161,107,35,148,169,160, 178,32,123,81,146,210,148,30,23,163,219,137,34,57,240,147,123,84,138,66,179,76,14,253,180,71,50,237,5,9,29,21,229,185,153,146, 115,233,20,157,228,206,92,201,89,194,21,113,70,156,61,125,34,191,113,246,12,223,143,253,198,101,237,183,223,133,229,226,182,103, 121,206,183,34,231,93,153,243,111,129,118,60,92,164,29,31,179,138,217,175,189,204,202,102,141,246,24,175,24,125,237,111,97, 215,104,15,80,197,236,205,252,81,54,185,254,255,252,3,243,31,208,130,120,3,0,0,0,0}; //============================================================================== #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ FIELD (authority, "authority", "Ljava/lang/String;") DECLARE_JNI_CLASS (AndroidProviderInfo, "android/content/pm/ProviderInfo") #undef JNI_CLASS_MEMBERS #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (constructor, "", "(Landroid/os/ParcelFileDescriptor;JJ)V") \ METHOD (createInputStream, "createInputStream", "()Ljava/io/FileInputStream;") \ METHOD (getLength, "getLength", "()J") DECLARE_JNI_CLASS (AssetFileDescriptor, "android/content/res/AssetFileDescriptor") #undef JNI_CLASS_MEMBERS #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (close, "close", "()V") DECLARE_JNI_CLASS (JavaCloseable, "java/io/Closeable") #undef JNI_CLASS_MEMBERS #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ STATICMETHOD (open, "open", "(Ljava/io/File;I)Landroid/os/ParcelFileDescriptor;") DECLARE_JNI_CLASS (ParcelFileDescriptor, "android/os/ParcelFileDescriptor") #undef JNI_CLASS_MEMBERS //============================================================================== class AndroidContentSharerCursor { public: class Owner { public: virtual ~Owner() {} virtual void cursorClosed (const AndroidContentSharerCursor&) = 0; }; AndroidContentSharerCursor (Owner& ownerToUse, JNIEnv* env, const LocalRef& contentProvider, const LocalRef& resultColumns) : owner (ownerToUse), 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() { return cursor.get(); } void cursorClosed() { MessageManager::callAsync ([this] { owner.cursorClosed (*this); }); } void addRow (LocalRef& values) { auto* env = getEnv(); env->CallVoidMethod (cursor.get(), JuceContentProviderCursor.addRow, values.get()); } private: Owner& owner; GlobalRef cursor; //============================================================================== #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (addRow, "addRow", "([Ljava/lang/Object;)V") \ METHOD (constructor, "", "(J[Ljava/lang/String;)V") \ CALLBACK (contentSharerCursorClosed, "contentSharerCursorClosed", "(J)V") \ DECLARE_JNI_CLASS_WITH_BYTECODE (JuceContentProviderCursor, "com/rmsl/juce/JuceContentProviderCursor", 16, javaJuceContentProviderCursor, sizeof (javaJuceContentProviderCursor)) #undef JNI_CLASS_MEMBERS static void JNICALL contentSharerCursorClosed (JNIEnv*, jobject, jlong host) { if (auto* myself = reinterpret_cast (host)) myself->cursorClosed(); } }; AndroidContentSharerCursor::JuceContentProviderCursor_Class AndroidContentSharerCursor::JuceContentProviderCursor; //============================================================================== class AndroidContentSharerFileObserver { public: class Owner { public: virtual ~Owner() {} virtual void fileHandleClosed (const AndroidContentSharerFileObserver&) = 0; }; AndroidContentSharerFileObserver (Owner& ownerToUse, JNIEnv* env, const LocalRef& contentProvider, const String& filepathToUse) : owner (ownerToUse), filepath (filepathToUse), fileObserver (GlobalRef (LocalRef (env->NewObject (JuceContentProviderFileObserver, JuceContentProviderFileObserver.constructor, reinterpret_cast (this), javaString (filepath).get(), open | access | closeWrite | closeNoWrite)))) { // the content provider must be created first jassert (contentProvider.get() != nullptr); env->CallVoidMethod (fileObserver, JuceContentProviderFileObserver.startWatching); } void onFileEvent (int event, const LocalRef& path) { ignoreUnused (path); if (event == open) { ++numOpenedHandles; } else if (event == access) { fileWasRead = true; } else if (event == closeNoWrite || event == closeWrite) { --numOpenedHandles; // numOpenedHandles may get negative if we don't receive open handle event. if (fileWasRead && numOpenedHandles <= 0) { MessageManager::callAsync ([this] { getEnv()->CallVoidMethod (fileObserver, JuceContentProviderFileObserver.stopWatching); owner.fileHandleClosed (*this); }); } } } private: static constexpr int open = 32; static constexpr int access = 1; static constexpr int closeWrite = 8; static constexpr int closeNoWrite = 16; bool fileWasRead = false; int numOpenedHandles = 0; Owner& owner; String filepath; GlobalRef fileObserver; //============================================================================== #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (constructor, "", "(JLjava/lang/String;I)V") \ METHOD (startWatching, "startWatching", "()V") \ METHOD (stopWatching, "stopWatching", "()V") \ CALLBACK (contentSharerFileObserverEvent, "contentSharerFileObserverEvent", "(JILjava/lang/String;)V") \ DECLARE_JNI_CLASS_WITH_BYTECODE (JuceContentProviderFileObserver, "com/rmsl/juce/JuceContentProviderFileObserver", 16, javaJuceContentProviderFileObserver, sizeof (javaJuceContentProviderFileObserver)) #undef JNI_CLASS_MEMBERS static void JNICALL contentSharerFileObserverEvent (JNIEnv*, jobject /*fileObserver*/, jlong host, int event, jstring path) { if (auto* myself = reinterpret_cast (host)) myself->onFileEvent (event, LocalRef (path)); } }; AndroidContentSharerFileObserver::JuceContentProviderFileObserver_Class AndroidContentSharerFileObserver::JuceContentProviderFileObserver; //============================================================================== class AndroidContentSharerPrepareFilesThread : private Thread { 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) { startThread(); } ~AndroidContentSharerPrepareFilesThread() override { signalThreadShouldExit(); waitForThreadToExit (10000); for (auto& f : temporaryFilesFromAssetFiles) f.deleteFile(); } jobject getResultFileUris() { return resultFileUris.get(); } const StringArray& getMimeTypes() const { return mimeTypes; } const StringArray& getFilePaths() const { return filePaths; } private: struct StreamCloser { StreamCloser (const LocalRef& 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 (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, {}); } auto* env = getEnv(); auto intent = LocalRef (env->NewObject (AndroidIntent, AndroidIntent.constructor)); env->CallObjectMethod (intent, AndroidIntent.setAction, javaString ("android.intent.action.SEND").get()); env->CallObjectMethod (intent, AndroidIntent.putExtra, javaString ("android.intent.extra.TEXT").get(), javaString (text).get()); env->CallObjectMethod (intent, AndroidIntent.setType, javaString ("text/plain").get()); auto chooserIntent = LocalRef (env->CallStaticObjectMethod (AndroidIntent, AndroidIntent.createChooser, intent.get(), javaString ("Choose share target").get())); WeakReference weakRef (this); startAndroidActivityForResult (chooserIntent, 1003, [weakRef] (int /*requestCode*/, int resultCode, LocalRef /*intentData*/) mutable { if (weakRef != nullptr) weakRef->sharingFinished (resultCode); }); } //============================================================================== void cursorClosed (const AndroidContentSharerCursor& cursor) override { cursors.removeObject (&cursor); } void fileHandleClosed (const AndroidContentSharerFileObserver&) override { decrementPendingFileCountAndNotifyOwnerIfReady(); } //============================================================================== jobject openFile (const LocalRef& contentProvider, const LocalRef& uri, const LocalRef& mode) { ignoreUnused (mode); WeakReference weakRef (this); if (weakRef == nullptr) return nullptr; auto* env = getEnv(); auto uriElements = getContentUriElements (env, uri); if (uriElements.filepath.isEmpty()) return nullptr; return getAssetFileDescriptor (env, contentProvider, uriElements.filepath); } jobject query (const LocalRef& contentProvider, const LocalRef& uri, const LocalRef& projection) { StringArray requestedColumns = javaStringArrayToJuce (projection); StringArray supportedColumns = getSupportedColumns(); StringArray resultColumns; for (const auto& col : supportedColumns) { if (requestedColumns.contains (col)) resultColumns.add (col); } // Unsupported columns were queried, file sharing may fail. if (resultColumns.isEmpty()) return nullptr; auto resultJavaColumns = juceStringArrayToJava (resultColumns); auto* env = getEnv(); auto cursor = cursors.add (new AndroidContentSharerCursor (*this, env, contentProvider, resultJavaColumns)); auto uriElements = getContentUriElements (env, uri); if (uriElements.filepath.isEmpty()) return cursor->getNativeCursor(); auto values = LocalRef (env->NewObjectArray ((jsize) resultColumns.size(), JavaObject, nullptr)); for (int i = 0; i < resultColumns.size(); ++i) { if (resultColumns.getReference (i) == "_display_name") { env->SetObjectArrayElement (values, i, javaString (uriElements.filename).get()); } else if (resultColumns.getReference (i) == "_size") { auto javaFile = LocalRef (env->NewObject (JavaFile, JavaFile.constructor, javaString (uriElements.filepath).get())); jlong fileLength = env->CallLongMethod (javaFile, JavaFile.length); env->SetObjectArrayElement (values, i, env->NewObject (JavaLong, JavaLong.constructor, fileLength)); } } cursor->addRow (values); return cursor->getNativeCursor(); } jobjectArray getStreamTypes (const LocalRef& uri, const LocalRef& mimeTypeFilter) { auto* env = getEnv(); auto extension = getContentUriElements (env, uri).filename.fromLastOccurrenceOf (".", false, true); if (extension.isEmpty()) return nullptr; return juceStringArrayToJava (filterMimeTypes (getMimeTypesForFileExtension (extension), juceString (mimeTypeFilter.get()))); } void sharingFinished (int resultCode) { sharingActivityDidFinish = true; succeeded = resultCode == -1; // Give content sharer a chance to request file access. if (nonAssetFilesPendingShare.get() == 0) startTimer (2000); else notifyOwnerIfReady(); } private: bool isContentSharingEnabled() const { auto* env = getEnv(); LocalRef 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)); if (providers == nullptr) return false; auto sharingContentProviderAuthority = packageName + ".sharingcontentprovider"; const int numProviders = env->GetArrayLength (providers.get()); for (int i = 0; i < numProviders; ++i) { auto providerInfo = LocalRef (env->GetObjectArrayElement (providers, i)); auto authority = LocalRef ((jstring) env->GetObjectField (providerInfo, AndroidProviderInfo.authority)); if (juceString (authority) == sharingContentProviderAuthority) return true; } return false; } void handleAsyncUpdate() override { jassert (prepareFilesThread != nullptr); if (prepareFilesThread == nullptr) return; filesPrepared (prepareFilesThread->getResultFileUris(), prepareFilesThread->getMimeTypes()); } void filesPrepared (jobject fileUris, const StringArray& mimeTypes) { auto* env = getEnv(); auto intent = LocalRef (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())); WeakReference weakRef (this); startAndroidActivityForResult (chooserIntent, 1003, [weakRef] (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; }; ContentUriElements getContentUriElements (JNIEnv* env, const LocalRef& uri) const { jassert (prepareFilesThread != nullptr); if (prepareFilesThread == nullptr) return {}; auto fullUri = juceString ((jstring) env->CallObjectMethod (uri.get(), AndroidUri.toString)); auto index = fullUri.fromFirstOccurrenceOf (uriBase, false, false) .upToFirstOccurrenceOf ("/", false, true); auto filename = fullUri.fromLastOccurrenceOf ("/", false, true); return { index, filename, prepareFilesThread->getFilePaths()[index.getIntValue()] }; } static StringArray getSupportedColumns() { return StringArray ("_display_name", "_size"); } jobject getAssetFileDescriptor (JNIEnv* env, const LocalRef& contentProvider, const String& filepath) { // This function can be called from multiple threads. { const ScopedLock sl (nonAssetFileOpenLock); if (! nonAssetFilePathsPendingShare.contains (filepath)) { nonAssetFilePathsPendingShare.add (filepath); ++nonAssetFilesPendingShare; nonAssetFileObservers.add (new AndroidContentSharerFileObserver (*this, env, contentProvider, filepath)); } } auto javaFile = LocalRef (env->NewObject (JavaFile, JavaFile.constructor, javaString (filepath).get())); constexpr int modeReadOnly = 268435456; auto parcelFileDescriptor = LocalRef (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; } String getCommonMimeType (const StringArray& mimeTypes) { if (mimeTypes.isEmpty()) return "*/*"; auto commonMime = mimeTypes[0]; bool lookForCommonGroup = false; for (int i = 1; i < mimeTypes.size(); ++i) { if (mimeTypes[i] == commonMime) continue; if (! lookForCommonGroup) { lookForCommonGroup = true; commonMime = commonMime.upToFirstOccurrenceOf ("/", true, false); } if (! mimeTypes[i].startsWith (commonMime)) return "*/*"; } return lookForCommonGroup ? commonMime + "*" : commonMime; } ContentSharer& owner; String packageName; String uriBase; std::unique_ptr prepareFilesThread; bool succeeded = false; String errorDescription; bool sharingActivityDidFinish = false; OwnedArray cursors; 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; } }; //============================================================================== ContentSharer::Pimpl* ContentSharer::createPimpl() { return new ContentSharerNativeImpl (*this); } ContentSharer::ContentSharerNativeImpl::JuceSharingContentProvider_Class ContentSharer::ContentSharerNativeImpl::JuceSharingContentProvider; } // namespace juce