Audio plugin host https://kx.studio/carla
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

juce_android_ContentSharer.cpp 38KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892
  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE 6 technical preview.
  4. Copyright (c) 2020 - Raw Material Software Limited
  5. You may use this code under the terms of the GPL v3
  6. (see www.gnu.org/licenses).
  7. For this technical preview, this file is not subject to commercial licensing.
  8. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  9. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  10. DISCLAIMED.
  11. ==============================================================================
  12. */
  13. namespace juce
  14. {
  15. //==============================================================================
  16. // This byte-code is generated from native/java/app/com/rmsl/juce/JuceContentProviderCursor.java with min sdk version 16
  17. // See juce_core/native/java/README.txt on how to generate this byte-code.
  18. static const uint8 javaJuceContentProviderCursor[] =
  19. {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,
  20. 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,
  21. 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,
  22. 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,
  23. 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,
  24. 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,
  25. 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,
  26. 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,
  27. 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,
  28. 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,
  29. 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,
  30. 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,
  31. 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,
  32. 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,
  33. 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,
  34. 212,119,165,154,238,157,124,243,170,142,213,255,224,55,143,234,50,200,64,3,0,0,0,0};
  35. // This byte-code is generated from native/java/app/com/rmsl/juce/JuceContentProviderFileObserver.java with min sdk version 16
  36. // See juce_core/native/java/README.txt on how to generate this byte-code.
  37. static const uint8 javaJuceContentProviderFileObserver[] =
  38. {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,
  39. 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,
  40. 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,
  41. 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,
  42. 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,
  43. 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,
  44. 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,
  45. 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,
  46. 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,
  47. 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,
  48. 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,
  49. 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,
  50. 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,
  51. 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,
  52. 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,
  53. 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,
  54. 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};
  55. //==============================================================================
  56. #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
  57. FIELD (authority, "authority", "Ljava/lang/String;")
  58. DECLARE_JNI_CLASS (AndroidProviderInfo, "android/content/pm/ProviderInfo")
  59. #undef JNI_CLASS_MEMBERS
  60. #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
  61. METHOD (constructor, "<init>", "(Landroid/os/ParcelFileDescriptor;JJ)V") \
  62. METHOD (createInputStream, "createInputStream", "()Ljava/io/FileInputStream;") \
  63. METHOD (getLength, "getLength", "()J")
  64. DECLARE_JNI_CLASS (AssetFileDescriptor, "android/content/res/AssetFileDescriptor")
  65. #undef JNI_CLASS_MEMBERS
  66. #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
  67. METHOD (close, "close", "()V")
  68. DECLARE_JNI_CLASS (JavaCloseable, "java/io/Closeable")
  69. #undef JNI_CLASS_MEMBERS
  70. #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
  71. STATICMETHOD (open, "open", "(Ljava/io/File;I)Landroid/os/ParcelFileDescriptor;")
  72. DECLARE_JNI_CLASS (ParcelFileDescriptor, "android/os/ParcelFileDescriptor")
  73. #undef JNI_CLASS_MEMBERS
  74. //==============================================================================
  75. class AndroidContentSharerCursor
  76. {
  77. public:
  78. class Owner
  79. {
  80. public:
  81. virtual ~Owner() {}
  82. virtual void cursorClosed (const AndroidContentSharerCursor&) = 0;
  83. };
  84. AndroidContentSharerCursor (Owner& ownerToUse, JNIEnv* env,
  85. const LocalRef<jobject>& contentProvider,
  86. const LocalRef<jobjectArray>& resultColumns)
  87. : owner (ownerToUse),
  88. cursor (GlobalRef (LocalRef<jobject> (env->NewObject (JuceContentProviderCursor,
  89. JuceContentProviderCursor.constructor,
  90. reinterpret_cast<jlong> (this),
  91. resultColumns.get()))))
  92. {
  93. // the content provider must be created first
  94. jassert (contentProvider.get() != nullptr);
  95. }
  96. jobject getNativeCursor() { return cursor.get(); }
  97. void cursorClosed()
  98. {
  99. MessageManager::callAsync ([this] { owner.cursorClosed (*this); });
  100. }
  101. void addRow (LocalRef<jobjectArray>& values)
  102. {
  103. auto* env = getEnv();
  104. env->CallVoidMethod (cursor.get(), JuceContentProviderCursor.addRow, values.get());
  105. }
  106. private:
  107. Owner& owner;
  108. GlobalRef cursor;
  109. //==============================================================================
  110. #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
  111. METHOD (addRow, "addRow", "([Ljava/lang/Object;)V") \
  112. METHOD (constructor, "<init>", "(J[Ljava/lang/String;)V") \
  113. CALLBACK (contentSharerCursorClosed, "contentSharerCursorClosed", "(J)V") \
  114. DECLARE_JNI_CLASS_WITH_BYTECODE (JuceContentProviderCursor, "com/rmsl/juce/JuceContentProviderCursor", 16, javaJuceContentProviderCursor, sizeof (javaJuceContentProviderCursor))
  115. #undef JNI_CLASS_MEMBERS
  116. static void JNICALL contentSharerCursorClosed (JNIEnv*, jobject, jlong host)
  117. {
  118. if (auto* myself = reinterpret_cast<AndroidContentSharerCursor*> (host))
  119. myself->cursorClosed();
  120. }
  121. };
  122. AndroidContentSharerCursor::JuceContentProviderCursor_Class AndroidContentSharerCursor::JuceContentProviderCursor;
  123. //==============================================================================
  124. class AndroidContentSharerFileObserver
  125. {
  126. public:
  127. class Owner
  128. {
  129. public:
  130. virtual ~Owner() {}
  131. virtual void fileHandleClosed (const AndroidContentSharerFileObserver&) = 0;
  132. };
  133. AndroidContentSharerFileObserver (Owner& ownerToUse, JNIEnv* env,
  134. const LocalRef<jobject>& contentProvider,
  135. const String& filepathToUse)
  136. : owner (ownerToUse),
  137. filepath (filepathToUse),
  138. fileObserver (GlobalRef (LocalRef<jobject> (env->NewObject (JuceContentProviderFileObserver,
  139. JuceContentProviderFileObserver.constructor,
  140. reinterpret_cast<jlong> (this),
  141. javaString (filepath).get(),
  142. open | access | closeWrite | closeNoWrite))))
  143. {
  144. // the content provider must be created first
  145. jassert (contentProvider.get() != nullptr);
  146. env->CallVoidMethod (fileObserver, JuceContentProviderFileObserver.startWatching);
  147. }
  148. void onFileEvent (int event, const LocalRef<jstring>& path)
  149. {
  150. ignoreUnused (path);
  151. if (event == open)
  152. {
  153. ++numOpenedHandles;
  154. }
  155. else if (event == access)
  156. {
  157. fileWasRead = true;
  158. }
  159. else if (event == closeNoWrite || event == closeWrite)
  160. {
  161. --numOpenedHandles;
  162. // numOpenedHandles may get negative if we don't receive open handle event.
  163. if (fileWasRead && numOpenedHandles <= 0)
  164. {
  165. MessageManager::callAsync ([this]
  166. {
  167. getEnv()->CallVoidMethod (fileObserver, JuceContentProviderFileObserver.stopWatching);
  168. owner.fileHandleClosed (*this);
  169. });
  170. }
  171. }
  172. }
  173. private:
  174. static constexpr int open = 32;
  175. static constexpr int access = 1;
  176. static constexpr int closeWrite = 8;
  177. static constexpr int closeNoWrite = 16;
  178. bool fileWasRead = false;
  179. int numOpenedHandles = 0;
  180. Owner& owner;
  181. String filepath;
  182. GlobalRef fileObserver;
  183. //==============================================================================
  184. #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
  185. METHOD (constructor, "<init>", "(JLjava/lang/String;I)V") \
  186. METHOD (startWatching, "startWatching", "()V") \
  187. METHOD (stopWatching, "stopWatching", "()V") \
  188. CALLBACK (contentSharerFileObserverEvent, "contentSharerFileObserverEvent", "(JILjava/lang/String;)V") \
  189. DECLARE_JNI_CLASS_WITH_BYTECODE (JuceContentProviderFileObserver, "com/rmsl/juce/JuceContentProviderFileObserver", 16, javaJuceContentProviderFileObserver, sizeof (javaJuceContentProviderFileObserver))
  190. #undef JNI_CLASS_MEMBERS
  191. static void JNICALL contentSharerFileObserverEvent (JNIEnv*, jobject /*fileObserver*/, jlong host, int event, jstring path)
  192. {
  193. if (auto* myself = reinterpret_cast<AndroidContentSharerFileObserver*> (host))
  194. myself->onFileEvent (event, LocalRef<jstring> (path));
  195. }
  196. };
  197. AndroidContentSharerFileObserver::JuceContentProviderFileObserver_Class AndroidContentSharerFileObserver::JuceContentProviderFileObserver;
  198. //==============================================================================
  199. class AndroidContentSharerPrepareFilesThread : private Thread
  200. {
  201. public:
  202. AndroidContentSharerPrepareFilesThread (AsyncUpdater& ownerToUse,
  203. const Array<URL>& fileUrlsToUse,
  204. const String& packageNameToUse,
  205. const String& uriBaseToUse)
  206. : Thread ("AndroidContentSharerPrepareFilesThread"),
  207. owner (ownerToUse),
  208. fileUrls (fileUrlsToUse),
  209. resultFileUris (GlobalRef (LocalRef<jobject> (getEnv()->NewObject (JavaArrayList,
  210. JavaArrayList.constructor,
  211. fileUrls.size())))),
  212. packageName (packageNameToUse),
  213. uriBase (uriBaseToUse)
  214. {
  215. startThread();
  216. }
  217. ~AndroidContentSharerPrepareFilesThread() override
  218. {
  219. signalThreadShouldExit();
  220. waitForThreadToExit (10000);
  221. for (auto& f : temporaryFilesFromAssetFiles)
  222. f.deleteFile();
  223. }
  224. jobject getResultFileUris() { return resultFileUris.get(); }
  225. const StringArray& getMimeTypes() const { return mimeTypes; }
  226. const StringArray& getFilePaths() const { return filePaths; }
  227. private:
  228. struct StreamCloser
  229. {
  230. StreamCloser (const LocalRef<jobject>& streamToUse)
  231. : stream (GlobalRef (streamToUse))
  232. {
  233. }
  234. ~StreamCloser()
  235. {
  236. if (stream.get() != nullptr)
  237. getEnv()->CallVoidMethod (stream, JavaCloseable.close);
  238. }
  239. GlobalRef stream;
  240. };
  241. void run() override
  242. {
  243. auto* env = getEnv();
  244. bool canSpecifyMimeTypes = true;
  245. for (auto f : fileUrls)
  246. {
  247. auto scheme = f.getScheme();
  248. // Only "file://" scheme or no scheme (for files in app bundle) are allowed!
  249. jassert (scheme.isEmpty() || scheme == "file");
  250. if (scheme.isEmpty())
  251. {
  252. // Raw resource names need to be all lower case
  253. jassert (f.toString (true).toLowerCase() == f.toString (true));
  254. // This will get us a file with file:// URI
  255. f = copyAssetFileToTemporaryFile (env, f.toString (true));
  256. if (f.isEmpty())
  257. continue;
  258. }
  259. if (threadShouldExit())
  260. return;
  261. auto filepath = URL::removeEscapeChars (f.toString (true).fromFirstOccurrenceOf ("file://", false, false));
  262. filePaths.add (filepath);
  263. auto filename = filepath.fromLastOccurrenceOf ("/", false, true);
  264. auto fileExtension = filename.fromLastOccurrenceOf (".", false, true);
  265. auto contentString = uriBase + String (filePaths.size() - 1) + "/" + filename;
  266. auto uri = LocalRef<jobject> (env->CallStaticObjectMethod (AndroidUri, AndroidUri.parse,
  267. javaString (contentString).get()));
  268. if (canSpecifyMimeTypes)
  269. canSpecifyMimeTypes = fileExtension.isNotEmpty();
  270. if (canSpecifyMimeTypes)
  271. mimeTypes.addArray (getMimeTypesForFileExtension (fileExtension));
  272. else
  273. mimeTypes.clear();
  274. env->CallBooleanMethod (resultFileUris, JavaArrayList.add, uri.get());
  275. }
  276. owner.triggerAsyncUpdate();
  277. }
  278. URL copyAssetFileToTemporaryFile (JNIEnv* env, const String& filename)
  279. {
  280. auto resources = LocalRef<jobject> (env->CallObjectMethod (getAppContext().get(), AndroidContext.getResources));
  281. int fileId = env->CallIntMethod (resources, AndroidResources.getIdentifier, javaString (filename).get(),
  282. javaString ("raw").get(), javaString (packageName).get());
  283. // Raw resource not found. Please make sure that you include your file as a raw resource
  284. // and that you specify just the file name, without an extension.
  285. jassert (fileId != 0);
  286. if (fileId == 0)
  287. return {};
  288. auto assetFd = LocalRef<jobject> (env->CallObjectMethod (resources,
  289. AndroidResources.openRawResourceFd,
  290. fileId));
  291. auto inputStream = StreamCloser (LocalRef<jobject> (env->CallObjectMethod (assetFd,
  292. AssetFileDescriptor.createInputStream)));
  293. if (jniCheckHasExceptionOccurredAndClear())
  294. {
  295. // Failed to open file stream for resource
  296. jassertfalse;
  297. return {};
  298. }
  299. auto tempFile = File::createTempFile ({});
  300. tempFile.createDirectory();
  301. tempFile = tempFile.getChildFile (filename);
  302. auto outputStream = StreamCloser (LocalRef<jobject> (env->NewObject (JavaFileOutputStream,
  303. JavaFileOutputStream.constructor,
  304. javaString (tempFile.getFullPathName()).get())));
  305. if (jniCheckHasExceptionOccurredAndClear())
  306. {
  307. // Failed to open file stream for temporary file
  308. jassertfalse;
  309. return {};
  310. }
  311. auto buffer = LocalRef<jbyteArray> (env->NewByteArray (1024));
  312. int bytesRead = 0;
  313. for (;;)
  314. {
  315. if (threadShouldExit())
  316. return {};
  317. bytesRead = env->CallIntMethod (inputStream.stream, JavaFileInputStream.read, buffer.get());
  318. if (jniCheckHasExceptionOccurredAndClear())
  319. {
  320. // Failed to read from resource file.
  321. jassertfalse;
  322. return {};
  323. }
  324. if (bytesRead < 0)
  325. break;
  326. env->CallVoidMethod (outputStream.stream, JavaFileOutputStream.write, buffer.get(), 0, bytesRead);
  327. if (jniCheckHasExceptionOccurredAndClear())
  328. {
  329. // Failed to write to temporary file.
  330. jassertfalse;
  331. return {};
  332. }
  333. }
  334. temporaryFilesFromAssetFiles.add (tempFile);
  335. return URL (tempFile);
  336. }
  337. AsyncUpdater& owner;
  338. Array<URL> fileUrls;
  339. GlobalRef resultFileUris;
  340. String packageName;
  341. String uriBase;
  342. StringArray filePaths;
  343. Array<File> temporaryFilesFromAssetFiles;
  344. StringArray mimeTypes;
  345. };
  346. //==============================================================================
  347. class ContentSharer::ContentSharerNativeImpl : public ContentSharer::Pimpl,
  348. public AndroidContentSharerFileObserver::Owner,
  349. public AndroidContentSharerCursor::Owner,
  350. public AsyncUpdater,
  351. private Timer
  352. {
  353. public:
  354. ContentSharerNativeImpl (ContentSharer& cs)
  355. : owner (cs),
  356. packageName (juceString (LocalRef<jstring> ((jstring) getEnv()->CallObjectMethod (getAppContext().get(), AndroidContext.getPackageName)))),
  357. uriBase ("content://" + packageName + ".sharingcontentprovider/")
  358. {
  359. }
  360. ~ContentSharerNativeImpl() override
  361. {
  362. masterReference.clear();
  363. }
  364. void shareFiles (const Array<URL>& files) override
  365. {
  366. if (! isContentSharingEnabled())
  367. {
  368. // You need to enable "Content Sharing" in Projucer's Android exporter.
  369. jassertfalse;
  370. owner.sharingFinished (false, {});
  371. }
  372. prepareFilesThread.reset (new AndroidContentSharerPrepareFilesThread (*this, files, packageName, uriBase));
  373. }
  374. void shareText (const String& text) override
  375. {
  376. if (! isContentSharingEnabled())
  377. {
  378. // You need to enable "Content Sharing" in Projucer's Android exporter.
  379. jassertfalse;
  380. owner.sharingFinished (false, {});
  381. }
  382. auto* env = getEnv();
  383. auto intent = LocalRef<jobject> (env->NewObject (AndroidIntent, AndroidIntent.constructor));
  384. env->CallObjectMethod (intent, AndroidIntent.setAction,
  385. javaString ("android.intent.action.SEND").get());
  386. env->CallObjectMethod (intent, AndroidIntent.putExtra,
  387. javaString ("android.intent.extra.TEXT").get(),
  388. javaString (text).get());
  389. env->CallObjectMethod (intent, AndroidIntent.setType, javaString ("text/plain").get());
  390. auto chooserIntent = LocalRef<jobject> (env->CallStaticObjectMethod (AndroidIntent, AndroidIntent.createChooser,
  391. intent.get(), javaString ("Choose share target").get()));
  392. WeakReference<ContentSharerNativeImpl> weakRef (this);
  393. startAndroidActivityForResult (chooserIntent, 1003,
  394. [weakRef] (int /*requestCode*/, int resultCode, LocalRef<jobject> /*intentData*/) mutable
  395. {
  396. if (weakRef != nullptr)
  397. weakRef->sharingFinished (resultCode);
  398. });
  399. }
  400. //==============================================================================
  401. void cursorClosed (const AndroidContentSharerCursor& cursor) override
  402. {
  403. cursors.removeObject (&cursor);
  404. }
  405. void fileHandleClosed (const AndroidContentSharerFileObserver&) override
  406. {
  407. decrementPendingFileCountAndNotifyOwnerIfReady();
  408. }
  409. //==============================================================================
  410. jobject openFile (const LocalRef<jobject>& contentProvider,
  411. const LocalRef<jobject>& uri, const LocalRef<jstring>& mode)
  412. {
  413. ignoreUnused (mode);
  414. WeakReference<ContentSharerNativeImpl> weakRef (this);
  415. if (weakRef == nullptr)
  416. return nullptr;
  417. auto* env = getEnv();
  418. auto uriElements = getContentUriElements (env, uri);
  419. if (uriElements.filepath.isEmpty())
  420. return nullptr;
  421. return getAssetFileDescriptor (env, contentProvider, uriElements.filepath);
  422. }
  423. jobject query (const LocalRef<jobject>& contentProvider, const LocalRef<jobject>& uri,
  424. const LocalRef<jobjectArray>& projection)
  425. {
  426. StringArray requestedColumns = javaStringArrayToJuce (projection);
  427. StringArray supportedColumns = getSupportedColumns();
  428. StringArray resultColumns;
  429. for (const auto& col : supportedColumns)
  430. {
  431. if (requestedColumns.contains (col))
  432. resultColumns.add (col);
  433. }
  434. // Unsupported columns were queried, file sharing may fail.
  435. if (resultColumns.isEmpty())
  436. return nullptr;
  437. auto resultJavaColumns = juceStringArrayToJava (resultColumns);
  438. auto* env = getEnv();
  439. auto cursor = cursors.add (new AndroidContentSharerCursor (*this, env, contentProvider,
  440. resultJavaColumns));
  441. auto uriElements = getContentUriElements (env, uri);
  442. if (uriElements.filepath.isEmpty())
  443. return cursor->getNativeCursor();
  444. auto values = LocalRef<jobjectArray> (env->NewObjectArray ((jsize) resultColumns.size(),
  445. JavaObject, nullptr));
  446. for (int i = 0; i < resultColumns.size(); ++i)
  447. {
  448. if (resultColumns.getReference (i) == "_display_name")
  449. {
  450. env->SetObjectArrayElement (values, i, javaString (uriElements.filename).get());
  451. }
  452. else if (resultColumns.getReference (i) == "_size")
  453. {
  454. auto javaFile = LocalRef<jobject> (env->NewObject (JavaFile, JavaFile.constructor,
  455. javaString (uriElements.filepath).get()));
  456. jlong fileLength = env->CallLongMethod (javaFile, JavaFile.length);
  457. env->SetObjectArrayElement (values, i, env->NewObject (JavaLong,
  458. JavaLong.constructor,
  459. fileLength));
  460. }
  461. }
  462. cursor->addRow (values);
  463. return cursor->getNativeCursor();
  464. }
  465. jobjectArray getStreamTypes (const LocalRef<jobject>& uri, const LocalRef<jstring>& mimeTypeFilter)
  466. {
  467. auto* env = getEnv();
  468. auto extension = getContentUriElements (env, uri).filename.fromLastOccurrenceOf (".", false, true);
  469. if (extension.isEmpty())
  470. return nullptr;
  471. return juceStringArrayToJava (filterMimeTypes (getMimeTypesForFileExtension (extension),
  472. juceString (mimeTypeFilter.get())));
  473. }
  474. void sharingFinished (int resultCode)
  475. {
  476. sharingActivityDidFinish = true;
  477. succeeded = resultCode == -1;
  478. // Give content sharer a chance to request file access.
  479. if (nonAssetFilesPendingShare.get() == 0)
  480. startTimer (2000);
  481. else
  482. notifyOwnerIfReady();
  483. }
  484. private:
  485. bool isContentSharingEnabled() const
  486. {
  487. auto* env = getEnv();
  488. LocalRef<jobject> packageManager (env->CallObjectMethod (getAppContext().get(), AndroidContext.getPackageManager));
  489. constexpr int getProviders = 8;
  490. auto packageInfo = LocalRef<jobject> (env->CallObjectMethod (packageManager,
  491. AndroidPackageManager.getPackageInfo,
  492. javaString (packageName).get(),
  493. getProviders));
  494. auto providers = LocalRef<jobjectArray> ((jobjectArray) env->GetObjectField (packageInfo,
  495. AndroidPackageInfo.providers));
  496. if (providers == nullptr)
  497. return false;
  498. auto sharingContentProviderAuthority = packageName + ".sharingcontentprovider";
  499. const int numProviders = env->GetArrayLength (providers.get());
  500. for (int i = 0; i < numProviders; ++i)
  501. {
  502. auto providerInfo = LocalRef<jobject> (env->GetObjectArrayElement (providers, i));
  503. auto authority = LocalRef<jstring> ((jstring) env->GetObjectField (providerInfo,
  504. AndroidProviderInfo.authority));
  505. if (juceString (authority) == sharingContentProviderAuthority)
  506. return true;
  507. }
  508. return false;
  509. }
  510. void handleAsyncUpdate() override
  511. {
  512. jassert (prepareFilesThread != nullptr);
  513. if (prepareFilesThread == nullptr)
  514. return;
  515. filesPrepared (prepareFilesThread->getResultFileUris(), prepareFilesThread->getMimeTypes());
  516. }
  517. void filesPrepared (jobject fileUris, const StringArray& mimeTypes)
  518. {
  519. auto* env = getEnv();
  520. auto intent = LocalRef<jobject> (env->NewObject (AndroidIntent, AndroidIntent.constructor));
  521. env->CallObjectMethod (intent, AndroidIntent.setAction,
  522. javaString ("android.intent.action.SEND_MULTIPLE").get());
  523. env->CallObjectMethod (intent, AndroidIntent.setType,
  524. javaString (getCommonMimeType (mimeTypes)).get());
  525. constexpr int grantReadPermission = 1;
  526. env->CallObjectMethod (intent, AndroidIntent.setFlags, grantReadPermission);
  527. env->CallObjectMethod (intent, AndroidIntent.putParcelableArrayListExtra,
  528. javaString ("android.intent.extra.STREAM").get(),
  529. fileUris);
  530. auto chooserIntent = LocalRef<jobject> (env->CallStaticObjectMethod (AndroidIntent,
  531. AndroidIntent.createChooser,
  532. intent.get(),
  533. javaString ("Choose share target").get()));
  534. WeakReference<ContentSharerNativeImpl> weakRef (this);
  535. startAndroidActivityForResult (chooserIntent, 1003,
  536. [weakRef] (int /*requestCode*/, int resultCode, LocalRef<jobject> /*intentData*/) mutable
  537. {
  538. if (weakRef != nullptr)
  539. weakRef->sharingFinished (resultCode);
  540. });
  541. }
  542. void decrementPendingFileCountAndNotifyOwnerIfReady()
  543. {
  544. --nonAssetFilesPendingShare;
  545. notifyOwnerIfReady();
  546. }
  547. void notifyOwnerIfReady()
  548. {
  549. if (sharingActivityDidFinish && nonAssetFilesPendingShare.get() == 0)
  550. owner.sharingFinished (succeeded, {});
  551. }
  552. void timerCallback() override
  553. {
  554. stopTimer();
  555. notifyOwnerIfReady();
  556. }
  557. //==============================================================================
  558. struct ContentUriElements
  559. {
  560. String index;
  561. String filename;
  562. String filepath;
  563. };
  564. ContentUriElements getContentUriElements (JNIEnv* env, const LocalRef<jobject>& uri) const
  565. {
  566. jassert (prepareFilesThread != nullptr);
  567. if (prepareFilesThread == nullptr)
  568. return {};
  569. auto fullUri = juceString ((jstring) env->CallObjectMethod (uri.get(), AndroidUri.toString));
  570. auto index = fullUri.fromFirstOccurrenceOf (uriBase, false, false)
  571. .upToFirstOccurrenceOf ("/", false, true);
  572. auto filename = fullUri.fromLastOccurrenceOf ("/", false, true);
  573. return { index, filename, prepareFilesThread->getFilePaths()[index.getIntValue()] };
  574. }
  575. static StringArray getSupportedColumns()
  576. {
  577. return StringArray ("_display_name", "_size");
  578. }
  579. jobject getAssetFileDescriptor (JNIEnv* env, const LocalRef<jobject>& contentProvider,
  580. const String& filepath)
  581. {
  582. // This function can be called from multiple threads.
  583. {
  584. const ScopedLock sl (nonAssetFileOpenLock);
  585. if (! nonAssetFilePathsPendingShare.contains (filepath))
  586. {
  587. nonAssetFilePathsPendingShare.add (filepath);
  588. ++nonAssetFilesPendingShare;
  589. nonAssetFileObservers.add (new AndroidContentSharerFileObserver (*this, env,
  590. contentProvider,
  591. filepath));
  592. }
  593. }
  594. auto javaFile = LocalRef<jobject> (env->NewObject (JavaFile, JavaFile.constructor,
  595. javaString (filepath).get()));
  596. constexpr int modeReadOnly = 268435456;
  597. auto parcelFileDescriptor = LocalRef<jobject> (env->CallStaticObjectMethod (ParcelFileDescriptor,
  598. ParcelFileDescriptor.open,
  599. javaFile.get(), modeReadOnly));
  600. if (jniCheckHasExceptionOccurredAndClear())
  601. {
  602. // Failed to create file descriptor. Have you provided a valid file path/resource name?
  603. jassertfalse;
  604. return nullptr;
  605. }
  606. jlong startOffset = 0;
  607. jlong unknownLength = -1;
  608. assetFileDescriptors.add (GlobalRef (LocalRef<jobject> (env->NewObject (AssetFileDescriptor,
  609. AssetFileDescriptor.constructor,
  610. parcelFileDescriptor.get(),
  611. startOffset, unknownLength))));
  612. return assetFileDescriptors.getReference (assetFileDescriptors.size() - 1).get();
  613. }
  614. StringArray filterMimeTypes (const StringArray& mimeTypes, const String& filter)
  615. {
  616. String filterToUse (filter.removeCharacters ("*"));
  617. if (filterToUse.isEmpty() || filterToUse == "/")
  618. return mimeTypes;
  619. StringArray result;
  620. for (const auto& type : mimeTypes)
  621. if (String (type).contains (filterToUse))
  622. result.add (type);
  623. return result;
  624. }
  625. String getCommonMimeType (const StringArray& mimeTypes)
  626. {
  627. if (mimeTypes.isEmpty())
  628. return "*/*";
  629. auto commonMime = mimeTypes[0];
  630. bool lookForCommonGroup = false;
  631. for (int i = 1; i < mimeTypes.size(); ++i)
  632. {
  633. if (mimeTypes[i] == commonMime)
  634. continue;
  635. if (! lookForCommonGroup)
  636. {
  637. lookForCommonGroup = true;
  638. commonMime = commonMime.upToFirstOccurrenceOf ("/", true, false);
  639. }
  640. if (! mimeTypes[i].startsWith (commonMime))
  641. return "*/*";
  642. }
  643. return lookForCommonGroup ? commonMime + "*" : commonMime;
  644. }
  645. ContentSharer& owner;
  646. String packageName;
  647. String uriBase;
  648. std::unique_ptr<AndroidContentSharerPrepareFilesThread> prepareFilesThread;
  649. bool succeeded = false;
  650. String errorDescription;
  651. bool sharingActivityDidFinish = false;
  652. OwnedArray<AndroidContentSharerCursor> cursors;
  653. Array<GlobalRef> assetFileDescriptors;
  654. CriticalSection nonAssetFileOpenLock;
  655. StringArray nonAssetFilePathsPendingShare;
  656. Atomic<int> nonAssetFilesPendingShare { 0 };
  657. OwnedArray<AndroidContentSharerFileObserver> nonAssetFileObservers;
  658. WeakReference<ContentSharerNativeImpl>::Master masterReference;
  659. friend class WeakReference<ContentSharerNativeImpl>;
  660. //==============================================================================
  661. #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
  662. CALLBACK (contentSharerQuery, "contentSharerQuery", "(Landroid/net/Uri;[Ljava/lang/String;)Landroid/database/Cursor;") \
  663. CALLBACK (contentSharerOpenFile, "contentSharerOpenFile", "(Landroid/net/Uri;Ljava/lang/String;)Landroid/content/res/AssetFileDescriptor;") \
  664. CALLBACK (contentSharerGetStreamTypes, "contentSharerGetStreamTypes", "(Landroid/net/Uri;Ljava/lang/String;)[Ljava/lang/String;") \
  665. DECLARE_JNI_CLASS_WITH_MIN_SDK (JuceSharingContentProvider, "com/rmsl/juce/JuceSharingContentProvider", 16)
  666. #undef JNI_CLASS_MEMBERS
  667. static jobject JNICALL contentSharerQuery (JNIEnv*, jobject contentProvider, jobject uri, jobjectArray projection)
  668. {
  669. if (auto *pimpl = (ContentSharer::ContentSharerNativeImpl *) ContentSharer::getInstance ()->pimpl.get ())
  670. return pimpl->query (LocalRef<jobject> (static_cast<jobject> (contentProvider)),
  671. LocalRef<jobject> (static_cast<jobject> (uri)),
  672. LocalRef<jobjectArray> (static_cast<jobjectArray> (projection)));
  673. return nullptr;
  674. }
  675. static jobject JNICALL contentSharerOpenFile (JNIEnv*, jobject contentProvider, jobject uri, jstring mode)
  676. {
  677. if (auto* pimpl = (ContentSharer::ContentSharerNativeImpl*) ContentSharer::getInstance()->pimpl.get())
  678. return pimpl->openFile (LocalRef<jobject> (static_cast<jobject> (contentProvider)),
  679. LocalRef<jobject> (static_cast<jobject> (uri)),
  680. LocalRef<jstring> (static_cast<jstring> (mode)));
  681. return nullptr;
  682. }
  683. static jobjectArray JNICALL contentSharerGetStreamTypes (JNIEnv*, jobject /*contentProvider*/, jobject uri, jstring mimeTypeFilter)
  684. {
  685. if (auto* pimpl = (ContentSharer::ContentSharerNativeImpl*) ContentSharer::getInstance()->pimpl.get())
  686. return pimpl->getStreamTypes (LocalRef<jobject> (static_cast<jobject> (uri)),
  687. LocalRef<jstring> (static_cast<jstring> (mimeTypeFilter)));
  688. return nullptr;
  689. }
  690. };
  691. //==============================================================================
  692. ContentSharer::Pimpl* ContentSharer::createPimpl()
  693. {
  694. return new ContentSharerNativeImpl (*this);
  695. }
  696. ContentSharer::ContentSharerNativeImpl::JuceSharingContentProvider_Class ContentSharer::ContentSharerNativeImpl::JuceSharingContentProvider;
  697. } // namespace juce