/* ============================================================================== This file is part of the JUCE 6 technical preview. Copyright (c) 2020 - Raw Material Software Limited You may use this code under the terms of the GPL v3 (see www.gnu.org/licenses). For this technical preview, this file is not subject to commercial licensing. 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