1 /*
2   ==============================================================================
3 
4    This file is part of the JUCE library.
5    Copyright (c) 2020 - Raw Material Software Limited
6 
7    JUCE is an open source library subject to commercial or open-source
8    licensing.
9 
10    The code included in this file is provided under the terms of the ISC license
11    http://www.isc.org/downloads/software-support-policy/isc-license. Permission
12    To use, copy, modify, and/or distribute this software for any purpose with or
13    without fee is hereby granted provided that the above copyright notice and
14    this permission notice appear in all copies.
15 
16    JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
17    EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
18    DISCLAIMED.
19 
20   ==============================================================================
21 */
22 
23 namespace juce
24 {
25 
26 #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
27   METHOD (constructor, "<init>",     "(Landroid/content/Context;Landroid/media/MediaScannerConnection$MediaScannerConnectionClient;)V") \
28   METHOD (connect,     "connect",    "()V") \
29   METHOD (disconnect,  "disconnect", "()V") \
30   METHOD (scanFile,    "scanFile",   "(Ljava/lang/String;Ljava/lang/String;)V") \
31 
32 DECLARE_JNI_CLASS (MediaScannerConnection, "android/media/MediaScannerConnection")
33 #undef JNI_CLASS_MEMBERS
34 
35 #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
36  METHOD (query,            "query",            "(Landroid/net/Uri;[Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;)Landroid/database/Cursor;") \
37  METHOD (openInputStream,  "openInputStream",  "(Landroid/net/Uri;)Ljava/io/InputStream;") \
38  METHOD (openOutputStream, "openOutputStream", "(Landroid/net/Uri;)Ljava/io/OutputStream;")
39 
40 DECLARE_JNI_CLASS (ContentResolver, "android/content/ContentResolver")
41 #undef JNI_CLASS_MEMBERS
42 
43 #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
44  METHOD (moveToFirst,     "moveToFirst",     "()Z") \
45  METHOD (getColumnIndex,  "getColumnIndex",  "(Ljava/lang/String;)I") \
46  METHOD (getString,       "getString",       "(I)Ljava/lang/String;") \
47  METHOD (close,           "close",           "()V") \
48 
49 DECLARE_JNI_CLASS (AndroidCursor, "android/database/Cursor")
50 #undef JNI_CLASS_MEMBERS
51 
52 #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
53  STATICMETHOD (getExternalStorageDirectory, "getExternalStorageDirectory", "()Ljava/io/File;") \
54  STATICMETHOD (getExternalStoragePublicDirectory, "getExternalStoragePublicDirectory", "(Ljava/lang/String;)Ljava/io/File;") \
55  STATICMETHOD (getDataDirectory, "getDataDirectory", "()Ljava/io/File;")
56 
57 DECLARE_JNI_CLASS (AndroidEnvironment, "android/os/Environment")
58 #undef JNI_CLASS_MEMBERS
59 
60 #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
61  METHOD (close, "close", "()V") \
62  METHOD (flush, "flush", "()V") \
63  METHOD (write, "write", "([BII)V")
64 
65 DECLARE_JNI_CLASS (AndroidOutputStream, "java/io/OutputStream")
66 #undef JNI_CLASS_MEMBERS
67 
68 #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
69  FIELD (publicSourceDir, "publicSourceDir", "Ljava/lang/String;") \
70  FIELD (dataDir, "dataDir", "Ljava/lang/String;")
71 
72 DECLARE_JNI_CLASS (AndroidApplicationInfo, "android/content/pm/ApplicationInfo")
73 #undef JNI_CLASS_MEMBERS
74 
75 //==============================================================================
juceFile(LocalRef<jobject> obj)76 static File juceFile (LocalRef<jobject> obj)
77 {
78     auto* env = getEnv();
79 
80     if (env->IsInstanceOf (obj.get(), JavaFile) != 0)
81         return File (juceString (LocalRef<jstring> ((jstring) env->CallObjectMethod (obj.get(),
82                                                                                      JavaFile.getAbsolutePath))));
83 
84     return {};
85 }
86 
getWellKnownFolder(const char * folderId)87 static File getWellKnownFolder (const char* folderId)
88 {
89     auto* env = getEnv();
90     auto fieldId = env->GetStaticFieldID (AndroidEnvironment, folderId, "Ljava/lang/String;");
91 
92     if (fieldId == nullptr)
93     {
94         // unknown field in environment
95         jassertfalse;
96         return {};
97     }
98 
99     LocalRef<jobject> fieldValue (env->GetStaticObjectField (AndroidEnvironment, fieldId));
100 
101     if (fieldValue == nullptr)
102         return {};
103 
104     LocalRef<jobject> downloadFolder (env->CallStaticObjectMethod (AndroidEnvironment,
105                                                                    AndroidEnvironment.getExternalStoragePublicDirectory,
106                                                                    fieldValue.get()));
107 
108     return (downloadFolder ? juceFile (downloadFolder) : File());
109 }
110 
urlToUri(const URL & url)111 static LocalRef<jobject> urlToUri (const URL& url)
112 {
113     return LocalRef<jobject> (getEnv()->CallStaticObjectMethod (AndroidUri, AndroidUri.parse,
114                                                                 javaString (url.toString (true)).get()));
115 }
116 
117 //==============================================================================
118 struct AndroidContentUriResolver
119 {
120 public:
getStreamForContentUrijuce::AndroidContentUriResolver121     static LocalRef<jobject> getStreamForContentUri (const URL& url, bool inputStream)
122     {
123         // only use this method for content URIs
124         jassert (url.getScheme() == "content");
125         auto* env = getEnv();
126 
127         LocalRef<jobject> contentResolver (env->CallObjectMethod (getAppContext().get(), AndroidContext.getContentResolver));
128 
129         if (contentResolver)
130             return LocalRef<jobject> ((env->CallObjectMethod (contentResolver.get(),
131                                                               inputStream ? ContentResolver.openInputStream
132                                                                           : ContentResolver.openOutputStream,
133                                                               urlToUri (url).get())));
134 
135         return LocalRef<jobject>();
136     }
137 
getLocalFileFromContentUrijuce::AndroidContentUriResolver138     static File getLocalFileFromContentUri (const URL& url)
139     {
140         // only use this method for content URIs
141         jassert (url.getScheme() == "content");
142 
143         auto authority  = url.getDomain();
144         auto documentId = URL::removeEscapeChars (url.getSubPath().fromFirstOccurrenceOf ("/", false, false));
145         auto tokens = StringArray::fromTokens (documentId, ":", "");
146 
147         if (authority == "com.android.externalstorage.documents")
148         {
149             auto storageId  = tokens[0];
150             auto subpath    = tokens[1];
151 
152             auto storagePath = getStorageDevicePath (storageId);
153 
154             if (storagePath != File())
155                 return storagePath.getChildFile (subpath);
156         }
157         else if (authority == "com.android.providers.downloads.documents")
158         {
159             auto type       = tokens[0];
160             auto downloadId = tokens[1];
161 
162             if (type.equalsIgnoreCase ("raw"))
163             {
164                 return File (downloadId);
165             }
166             else if (type.equalsIgnoreCase ("downloads"))
167             {
168                 auto subDownloadPath = url.getSubPath().fromFirstOccurrenceOf ("tree/downloads", false, false);
169                 return File (getWellKnownFolder ("DIRECTORY_DOWNLOADS").getFullPathName() + "/" + subDownloadPath);
170             }
171             else
172             {
173                 return getLocalFileFromContentUri (URL ("content://downloads/public_downloads/" + documentId));
174             }
175         }
176         else if (authority == "com.android.providers.media.documents" && documentId.isNotEmpty())
177         {
178             auto type    = tokens[0];
179             auto mediaId = tokens[1];
180 
181             if (type == "image")
182                 type = "images";
183 
184             return getCursorDataColumn (URL ("content://media/external/" + type + "/media"),
185                                         "_id=?", StringArray {mediaId});
186         }
187 
188         return getCursorDataColumn (url);
189     }
190 
getFileNameFromContentUrijuce::AndroidContentUriResolver191     static String getFileNameFromContentUri (const URL& url)
192     {
193         auto uri = urlToUri (url);
194         auto* env = getEnv();
195         LocalRef<jobject> contentResolver (env->CallObjectMethod (getAppContext().get(), AndroidContext.getContentResolver));
196 
197         if (contentResolver == nullptr)
198             return {};
199 
200         auto filename = getStringUsingDataColumn ("_display_name", env, uri, contentResolver);
201 
202         // Fallback to "_data" column
203         if (filename.isEmpty())
204         {
205             auto path = getStringUsingDataColumn ("_data", env, uri, contentResolver);
206             filename = path.fromLastOccurrenceOf ("/", false, true);
207         }
208 
209         return filename;
210     }
211 
212 private:
213     //==============================================================================
getCursorDataColumnjuce::AndroidContentUriResolver214     static String getCursorDataColumn (const URL& url, const String& selection = {},
215                                        const StringArray& selectionArgs = {})
216     {
217         auto uri = urlToUri (url);
218         auto* env = getEnv();
219         LocalRef<jobject> contentResolver (env->CallObjectMethod (getAppContext().get(), AndroidContext.getContentResolver));
220 
221         if (contentResolver)
222         {
223             LocalRef<jstring> columnName (javaString ("_data"));
224             LocalRef<jobjectArray> projection (env->NewObjectArray (1, JavaString, columnName.get()));
225 
226             LocalRef<jobjectArray> args;
227 
228             if (selection.isNotEmpty())
229             {
230                 args = LocalRef<jobjectArray> (env->NewObjectArray (selectionArgs.size(), JavaString, javaString ("").get()));
231 
232                 for (int i = 0; i < selectionArgs.size(); ++i)
233                     env->SetObjectArrayElement (args.get(), i, javaString (selectionArgs[i]).get());
234             }
235 
236             LocalRef<jstring> jSelection (selection.isNotEmpty() ? javaString (selection) : LocalRef<jstring>());
237             LocalRef<jobject> cursor (env->CallObjectMethod (contentResolver.get(), ContentResolver.query,
238                                                              uri.get(), projection.get(), jSelection.get(),
239                                                              args.get(), nullptr));
240 
241             if (jniCheckHasExceptionOccurredAndClear())
242             {
243                 // An exception has occurred, have you acquired RuntimePermission::readExternalStorage permission?
244                 jassertfalse;
245                 return {};
246             }
247 
248             if (cursor)
249             {
250                 if (env->CallBooleanMethod (cursor.get(), AndroidCursor.moveToFirst) != 0)
251                 {
252                     auto columnIndex = env->CallIntMethod (cursor.get(), AndroidCursor.getColumnIndex, columnName.get());
253 
254                     if (columnIndex >= 0)
255                     {
256                         LocalRef<jstring> value ((jstring) env->CallObjectMethod (cursor.get(), AndroidCursor.getString, columnIndex));
257 
258                         if (value)
259                             return juceString (value.get());
260                     }
261                 }
262 
263                 env->CallVoidMethod (cursor.get(), AndroidCursor.close);
264             }
265         }
266 
267         return {};
268     }
269 
270     //==============================================================================
getStorageDevicePathjuce::AndroidContentUriResolver271     static File getStorageDevicePath (const String& storageId)
272     {
273         // check for the primary alias
274         if (storageId == "primary")
275             return getPrimaryStorageDirectory();
276 
277         auto storageDevices = getSecondaryStorageDirectories();
278 
279         for (auto storageDevice : storageDevices)
280             if (getStorageIdForMountPoint (storageDevice) == storageId)
281                 return storageDevice;
282 
283         return {};
284     }
285 
getPrimaryStorageDirectoryjuce::AndroidContentUriResolver286     static File getPrimaryStorageDirectory()
287     {
288         auto* env = getEnv();
289         return juceFile (LocalRef<jobject> (env->CallStaticObjectMethod (AndroidEnvironment, AndroidEnvironment.getExternalStorageDirectory)));
290     }
291 
getSecondaryStorageDirectoriesjuce::AndroidContentUriResolver292     static Array<File> getSecondaryStorageDirectories()
293     {
294         Array<File> results;
295 
296         if (getAndroidSDKVersion() >= 19)
297         {
298             auto* env = getEnv();
299             static jmethodID m = (env->GetMethodID (AndroidContext, "getExternalFilesDirs",
300                                                     "(Ljava/lang/String;)[Ljava/io/File;"));
301             if (m == nullptr)
302                 return {};
303 
304             auto paths = convertFileArray (LocalRef<jobject> (env->CallObjectMethod (getAppContext().get(), m, nullptr)));
305 
306             for (auto path : paths)
307                 results.add (getMountPointForFile (path));
308         }
309         else
310         {
311             // on older SDKs other external storages are located "next" to the primary
312             // storage mount point
313             auto mountFolder = getMountPointForFile (getPrimaryStorageDirectory())
314                                     .getParentDirectory();
315 
316             // don't include every folder. Only folders which are actually mountpoints
317             juce_statStruct info;
318             if (! juce_stat (mountFolder.getFullPathName(), info))
319                 return {};
320 
321             auto rootFsDevice = info.st_dev;
322 
323             for (const auto& iter : RangedDirectoryIterator (mountFolder, false, "*", File::findDirectories))
324             {
325                 auto candidate = iter.getFile();
326 
327                 if (juce_stat (candidate.getFullPathName(), info)
328                       && info.st_dev != rootFsDevice)
329                     results.add (candidate);
330             }
331 
332         }
333 
334         return results;
335     }
336 
337     //==============================================================================
getStorageIdForMountPointjuce::AndroidContentUriResolver338     static String getStorageIdForMountPoint (const File& mountpoint)
339     {
340         // currently this seems to work fine, but something
341         // more intelligent may be needed in the future
342         return mountpoint.getFileName();
343     }
344 
getMountPointForFilejuce::AndroidContentUriResolver345     static File getMountPointForFile (const File& file)
346     {
347         juce_statStruct info;
348 
349         if (juce_stat (file.getFullPathName(), info))
350         {
351             auto dev  = info.st_dev;
352             File mountPoint = file;
353 
354             for (;;)
355             {
356                 auto parent = mountPoint.getParentDirectory();
357 
358                 if (parent == mountPoint)
359                     break;
360 
361                 juce_stat (parent.getFullPathName(), info);
362 
363                 if (info.st_dev != dev)
364                     break;
365 
366                 mountPoint = parent;
367             }
368 
369             return mountPoint;
370         }
371 
372         return {};
373     }
374 
375     //==============================================================================
convertFileArrayjuce::AndroidContentUriResolver376     static Array<File> convertFileArray (LocalRef<jobject> obj)
377     {
378         auto* env = getEnv();
379         int n = (int) env->GetArrayLength ((jobjectArray) obj.get());
380         Array<File> files;
381 
382         for (int i = 0; i < n; ++i)
383             files.add (juceFile (LocalRef<jobject> (env->GetObjectArrayElement ((jobjectArray) obj.get(),
384                                                                                  (jsize) i))));
385 
386         return files;
387     }
388 
389     //==============================================================================
getStringUsingDataColumnjuce::AndroidContentUriResolver390     static String getStringUsingDataColumn (const String& columnNameToUse, JNIEnv* env,
391                                             const LocalRef<jobject>& uri,
392                                             const LocalRef<jobject>& contentResolver)
393     {
394         LocalRef<jstring> columnName (javaString (columnNameToUse));
395         LocalRef<jobjectArray> projection (env->NewObjectArray (1, JavaString, columnName.get()));
396 
397         LocalRef<jobject> cursor (env->CallObjectMethod (contentResolver.get(), ContentResolver.query,
398                                                          uri.get(), projection.get(), nullptr,
399                                                          nullptr, nullptr));
400 
401         if (jniCheckHasExceptionOccurredAndClear())
402         {
403             // An exception has occurred, have you acquired RuntimePermission::readExternalStorage permission?
404             jassertfalse;
405             return {};
406         }
407 
408         if (cursor == nullptr)
409             return {};
410 
411         String fileName;
412 
413         if (env->CallBooleanMethod (cursor.get(), AndroidCursor.moveToFirst) != 0)
414         {
415             auto columnIndex = env->CallIntMethod (cursor.get(), AndroidCursor.getColumnIndex, columnName.get());
416 
417             if (columnIndex >= 0)
418             {
419                 LocalRef<jstring> value ((jstring) env->CallObjectMethod (cursor.get(), AndroidCursor.getString, columnIndex));
420 
421                 if (value)
422                     fileName = juceString (value.get());
423 
424             }
425         }
426 
427         env->CallVoidMethod (cursor.get(), AndroidCursor.close);
428 
429         return fileName;
430     }
431 };
432 
433 //==============================================================================
434 struct AndroidContentUriOutputStream :  public OutputStream
435 {
AndroidContentUriOutputStreamjuce::AndroidContentUriOutputStream436     AndroidContentUriOutputStream (LocalRef<jobject>&& outputStream)
437         : stream (outputStream)
438     {
439     }
440 
~AndroidContentUriOutputStreamjuce::AndroidContentUriOutputStream441     ~AndroidContentUriOutputStream() override
442     {
443         stream.callVoidMethod (AndroidOutputStream.close);
444     }
445 
flushjuce::AndroidContentUriOutputStream446     void flush() override
447     {
448         stream.callVoidMethod (AndroidOutputStream.flush);
449     }
450 
setPositionjuce::AndroidContentUriOutputStream451     bool setPosition (int64 newPos) override
452     {
453         return (newPos == pos);
454     }
455 
getPositionjuce::AndroidContentUriOutputStream456     int64 getPosition() override
457     {
458         return pos;
459     }
460 
writejuce::AndroidContentUriOutputStream461     bool write (const void* dataToWrite, size_t numberOfBytes) override
462     {
463         if (numberOfBytes == 0)
464             return true;
465 
466         JNIEnv* env = getEnv();
467 
468         jbyteArray javaArray = env->NewByteArray ((jsize) numberOfBytes);
469         env->SetByteArrayRegion (javaArray, 0, (jsize) numberOfBytes, (const jbyte*) dataToWrite);
470 
471         stream.callVoidMethod (AndroidOutputStream.write, javaArray, 0, (jint) numberOfBytes);
472         env->DeleteLocalRef (javaArray);
473 
474         pos += static_cast<int64> (numberOfBytes);
475         return true;
476     }
477 
478     GlobalRef stream;
479     int64 pos = 0;
480 };
481 
juce_CreateContentURIOutputStream(const URL & url)482 OutputStream* juce_CreateContentURIOutputStream (const URL& url)
483 {
484     auto stream = AndroidContentUriResolver::getStreamForContentUri (url, false);
485 
486     return (stream.get() != nullptr ? new AndroidContentUriOutputStream (std::move (stream)) : nullptr);
487 }
488 
489 //==============================================================================
490 class MediaScannerConnectionClient : public AndroidInterfaceImplementer
491 {
492 public:
493     virtual void onMediaScannerConnected() = 0;
494     virtual void onScanCompleted() = 0;
495 
496 private:
invoke(jobject proxy,jobject method,jobjectArray args)497     jobject invoke (jobject proxy, jobject method, jobjectArray args) override
498     {
499         auto* env = getEnv();
500 
501         auto methodName = juceString ((jstring) env->CallObjectMethod (method, JavaMethod.getName));
502 
503         if (methodName == "onMediaScannerConnected")
504         {
505             onMediaScannerConnected();
506             return nullptr;
507         }
508         else if (methodName == "onScanCompleted")
509         {
510             onScanCompleted();
511             return nullptr;
512         }
513 
514         return AndroidInterfaceImplementer::invoke (proxy, method, args);
515     }
516 };
517 
518 //==============================================================================
isOnCDRomDrive() const519 bool File::isOnCDRomDrive() const
520 {
521     return false;
522 }
523 
isOnHardDisk() const524 bool File::isOnHardDisk() const
525 {
526     return true;
527 }
528 
isOnRemovableDrive() const529 bool File::isOnRemovableDrive() const
530 {
531     return false;
532 }
533 
getVersion() const534 String File::getVersion() const
535 {
536     return {};
537 }
538 
getDocumentsDirectory()539 static File getDocumentsDirectory()
540 {
541     auto* env = getEnv();
542 
543     if (getAndroidSDKVersion() >= 19)
544         return getWellKnownFolder ("DIRECTORY_DOCUMENTS");
545 
546     return juceFile (LocalRef<jobject> (env->CallStaticObjectMethod (AndroidEnvironment, AndroidEnvironment.getDataDirectory)));
547 }
548 
getAppDataDir(bool dataDir)549 static File getAppDataDir (bool dataDir)
550 {
551     auto* env = getEnv();
552 
553     LocalRef<jobject> applicationInfo (env->CallObjectMethod (getAppContext().get(), AndroidContext.getApplicationInfo));
554     LocalRef<jobject> jString (env->GetObjectField (applicationInfo.get(), dataDir ? AndroidApplicationInfo.dataDir : AndroidApplicationInfo.publicSourceDir));
555 
556     return  {juceString ((jstring) jString.get())};
557 }
558 
getSpecialLocation(const SpecialLocationType type)559 File File::getSpecialLocation (const SpecialLocationType type)
560 {
561     switch (type)
562     {
563         case userHomeDirectory:
564         case userApplicationDataDirectory:
565         case userDesktopDirectory:
566         case commonApplicationDataDirectory:
567         {
568             static File appDataDir = getAppDataDir (true);
569             return appDataDir;
570         }
571 
572         case userDocumentsDirectory:
573         case commonDocumentsDirectory:
574         {
575             static auto docsDir = getDocumentsDirectory();
576             return docsDir;
577         }
578 
579         case userPicturesDirectory:
580         {
581             static auto picturesDir = getWellKnownFolder ("DIRECTORY_PICTURES");
582             return picturesDir;
583         }
584 
585         case userMusicDirectory:
586         {
587             static auto musicDir = getWellKnownFolder ("DIRECTORY_MUSIC");
588             return musicDir;
589         }
590         case userMoviesDirectory:
591         {
592             static auto moviesDir = getWellKnownFolder ("DIRECTORY_MOVIES");
593             return moviesDir;
594         }
595 
596         case globalApplicationsDirectory:
597             return File ("/system/app");
598 
599         case tempDirectory:
600         {
601             File tmp = getSpecialLocation (commonApplicationDataDirectory).getChildFile (".temp");
602             tmp.createDirectory();
603             return File (tmp.getFullPathName());
604         }
605 
606         case invokedExecutableFile:
607         case currentExecutableFile:
608         case currentApplicationFile:
609         case hostApplicationPath:
610             return getAppDataDir (false);
611 
612         default:
613             jassertfalse; // unknown type?
614             break;
615     }
616 
617     return {};
618 }
619 
moveToTrash() const620 bool File::moveToTrash() const
621 {
622     if (! exists())
623         return true;
624 
625     // TODO
626     return false;
627 }
628 
openDocument(const String & fileName,const String &)629 JUCE_API bool JUCE_CALLTYPE Process::openDocument (const String& fileName, const String&)
630 {
631     URL targetURL (fileName);
632     auto* env = getEnv();
633 
634     const LocalRef<jstring> action (javaString ("android.intent.action.VIEW"));
635     LocalRef<jobject> intent (env->NewObject (AndroidIntent, AndroidIntent.constructWithUri, action.get(), urlToUri (targetURL).get()));
636 
637     env->CallVoidMethod (getCurrentActivity(), AndroidContext.startActivity, intent.get());
638     return true;
639 }
640 
revealToUser() const641 void File::revealToUser() const
642 {
643 }
644 
645 //==============================================================================
646 class SingleMediaScanner : public MediaScannerConnectionClient
647 {
648 public:
SingleMediaScanner(const String & filename)649     SingleMediaScanner (const String& filename)
650         : msc (LocalRef<jobject> (getEnv()->NewObject (MediaScannerConnection,
651                                                        MediaScannerConnection.constructor,
652                                                        getAppContext().get(),
653                                                        CreateJavaInterface (this, "android/media/MediaScannerConnection$MediaScannerConnectionClient").get()))),
654           file (filename)
655     {
656         getEnv()->CallVoidMethod (msc.get(), MediaScannerConnection.connect);
657     }
658 
onMediaScannerConnected()659     void onMediaScannerConnected() override
660     {
661         auto* env = getEnv();
662 
663         env->CallVoidMethod (msc.get(), MediaScannerConnection.scanFile, javaString (file).get(), 0);
664     }
665 
onScanCompleted()666     void onScanCompleted() override
667     {
668         getEnv()->CallVoidMethod (msc.get(), MediaScannerConnection.disconnect);
669     }
670 
671 private:
672     GlobalRef msc;
673     String file;
674 };
675 
flushInternal()676 void FileOutputStream::flushInternal()
677 {
678     if (fileHandle != nullptr)
679     {
680         if (fsync (getFD (fileHandle)) == -1)
681             status = getResultForErrno();
682 
683         // This stuff tells the OS to asynchronously update the metadata
684         // that the OS has cached about the file - this metadata is used
685         // when the device is acting as a USB drive, and unless it's explicitly
686         // refreshed, it'll get out of step with the real file.
687         new SingleMediaScanner (file.getFullPathName());
688     }
689 }
690 
691 } // namespace juce
692