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