NativeGallery.cs 34 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051
  1. using System;
  2. using System.Globalization;
  3. using System.IO;
  4. using UnityEngine;
  5. #if UNITY_2018_4_OR_NEWER && !NATIVE_GALLERY_DISABLE_ASYNC_FUNCTIONS
  6. using System.Threading.Tasks;
  7. using Unity.Collections;
  8. using UnityEngine.Networking;
  9. #endif
  10. #if UNITY_ANDROID || UNITY_IOS
  11. using NativeGalleryNamespace;
  12. #endif
  13. using Object = UnityEngine.Object;
  14. public static class NativeGallery
  15. {
  16. public struct ImageProperties
  17. {
  18. public readonly int width;
  19. public readonly int height;
  20. public readonly string mimeType;
  21. public readonly ImageOrientation orientation;
  22. public ImageProperties( int width, int height, string mimeType, ImageOrientation orientation )
  23. {
  24. this.width = width;
  25. this.height = height;
  26. this.mimeType = mimeType;
  27. this.orientation = orientation;
  28. }
  29. }
  30. public struct VideoProperties
  31. {
  32. public readonly int width;
  33. public readonly int height;
  34. public readonly long duration;
  35. public readonly float rotation;
  36. public VideoProperties( int width, int height, long duration, float rotation )
  37. {
  38. this.width = width;
  39. this.height = height;
  40. this.duration = duration;
  41. this.rotation = rotation;
  42. }
  43. }
  44. public enum PermissionType { Read = 0, Write = 1 };
  45. public enum Permission { Denied = 0, Granted = 1, ShouldAsk = 2 };
  46. [Flags]
  47. public enum MediaType { Image = 1, Video = 2, Audio = 4 };
  48. // EXIF orientation: http://sylvana.net/jpegcrop/exif_orientation.html (indices are reordered)
  49. public enum ImageOrientation { Unknown = -1, Normal = 0, Rotate90 = 1, Rotate180 = 2, Rotate270 = 3, FlipHorizontal = 4, Transpose = 5, FlipVertical = 6, Transverse = 7 };
  50. public delegate void MediaSaveCallback( bool success, string path );
  51. public delegate void MediaPickCallback( string path );
  52. public delegate void MediaPickMultipleCallback( string[] paths );
  53. #region Platform Specific Elements
  54. #if !UNITY_EDITOR && UNITY_ANDROID
  55. private static AndroidJavaClass m_ajc = null;
  56. private static AndroidJavaClass AJC
  57. {
  58. get
  59. {
  60. if( m_ajc == null )
  61. m_ajc = new AndroidJavaClass( "com.yasirkula.unity.NativeGallery" );
  62. return m_ajc;
  63. }
  64. }
  65. private static AndroidJavaObject m_context = null;
  66. private static AndroidJavaObject Context
  67. {
  68. get
  69. {
  70. if( m_context == null )
  71. {
  72. using( AndroidJavaObject unityClass = new AndroidJavaClass( "com.unity3d.player.UnityPlayer" ) )
  73. {
  74. m_context = unityClass.GetStatic<AndroidJavaObject>( "currentActivity" );
  75. }
  76. }
  77. return m_context;
  78. }
  79. }
  80. #elif !UNITY_EDITOR && UNITY_IOS
  81. [System.Runtime.InteropServices.DllImport( "__Internal" )]
  82. private static extern int _NativeGallery_CheckPermission( int readPermission, int permissionFreeMode );
  83. [System.Runtime.InteropServices.DllImport( "__Internal" )]
  84. private static extern int _NativeGallery_RequestPermission( int readPermission, int permissionFreeMode );
  85. [System.Runtime.InteropServices.DllImport( "__Internal" )]
  86. private static extern void _NativeGallery_ShowLimitedLibraryPicker();
  87. [System.Runtime.InteropServices.DllImport( "__Internal" )]
  88. private static extern int _NativeGallery_CanOpenSettings();
  89. [System.Runtime.InteropServices.DllImport( "__Internal" )]
  90. private static extern void _NativeGallery_OpenSettings();
  91. [System.Runtime.InteropServices.DllImport( "__Internal" )]
  92. private static extern int _NativeGallery_CanPickMultipleMedia();
  93. [System.Runtime.InteropServices.DllImport( "__Internal" )]
  94. private static extern int _NativeGallery_GetMediaTypeFromExtension( string extension );
  95. [System.Runtime.InteropServices.DllImport( "__Internal" )]
  96. private static extern void _NativeGallery_ImageWriteToAlbum( string path, string album, int permissionFreeMode );
  97. [System.Runtime.InteropServices.DllImport( "__Internal" )]
  98. private static extern void _NativeGallery_VideoWriteToAlbum( string path, string album, int permissionFreeMode );
  99. [System.Runtime.InteropServices.DllImport( "__Internal" )]
  100. private static extern void _NativeGallery_PickMedia( string mediaSavePath, int mediaType, int permissionFreeMode, int selectionLimit );
  101. [System.Runtime.InteropServices.DllImport( "__Internal" )]
  102. private static extern string _NativeGallery_GetImageProperties( string path );
  103. [System.Runtime.InteropServices.DllImport( "__Internal" )]
  104. private static extern string _NativeGallery_GetVideoProperties( string path );
  105. [System.Runtime.InteropServices.DllImport( "__Internal" )]
  106. private static extern string _NativeGallery_GetVideoThumbnail( string path, string thumbnailSavePath, int maxSize, double captureTimeInSeconds );
  107. [System.Runtime.InteropServices.DllImport( "__Internal" )]
  108. private static extern string _NativeGallery_LoadImageAtPath( string path, string temporaryFilePath, int maxSize );
  109. #endif
  110. #if !UNITY_EDITOR && ( UNITY_ANDROID || UNITY_IOS )
  111. private static string m_temporaryImagePath = null;
  112. private static string TemporaryImagePath
  113. {
  114. get
  115. {
  116. if( m_temporaryImagePath == null )
  117. {
  118. m_temporaryImagePath = Path.Combine( Application.temporaryCachePath, "tmpImg" );
  119. Directory.CreateDirectory( Application.temporaryCachePath );
  120. }
  121. return m_temporaryImagePath;
  122. }
  123. }
  124. private static string m_selectedMediaPath = null;
  125. private static string SelectedMediaPath
  126. {
  127. get
  128. {
  129. if( m_selectedMediaPath == null )
  130. {
  131. m_selectedMediaPath = Path.Combine( Application.temporaryCachePath, "pickedMedia" );
  132. Directory.CreateDirectory( Application.temporaryCachePath );
  133. }
  134. return m_selectedMediaPath;
  135. }
  136. }
  137. #endif
  138. #endregion
  139. #region Runtime Permissions
  140. // PermissionFreeMode was initially planned to be a toggleable setting on iOS but it has its own issues when set to false, so its value is forced to true.
  141. // These issues are:
  142. // - Presented permission dialog will have a "Select Photos" option on iOS 14+ but clicking it will freeze and eventually crash the app (I'm guessing that
  143. // this is caused by how permissions are handled synchronously in NativeGallery)
  144. // - While saving images/videos to Photos, iOS 14+ users would see the "Select Photos" option (which is irrelevant in this context, hence confusing) and
  145. // the user must grant full Photos access in order to save the image/video to a custom album
  146. // The only downside of having PermissionFreeMode = true is that, on iOS 14+, images/videos will be saved to the default Photos album rather than the
  147. // provided custom album
  148. private const bool PermissionFreeMode = true;
  149. public static Permission CheckPermission( PermissionType permissionType, MediaType mediaTypes )
  150. {
  151. #if !UNITY_EDITOR && UNITY_ANDROID
  152. Permission result = (Permission) AJC.CallStatic<int>( "CheckPermission", Context, permissionType == PermissionType.Read, (int) mediaTypes );
  153. if( result == Permission.Denied && (Permission) PlayerPrefs.GetInt( "NativeGalleryPermission", (int) Permission.ShouldAsk ) == Permission.ShouldAsk )
  154. result = Permission.ShouldAsk;
  155. return result;
  156. #elif !UNITY_EDITOR && UNITY_IOS
  157. // result == 3: LimitedAccess permission on iOS, no need to handle it when PermissionFreeMode is set to true
  158. int result = _NativeGallery_CheckPermission( permissionType == PermissionType.Read ? 1 : 0, PermissionFreeMode ? 1 : 0 );
  159. return result == 3 ? Permission.Granted : (Permission) result;
  160. #else
  161. return Permission.Granted;
  162. #endif
  163. }
  164. public static Permission RequestPermission( PermissionType permissionType, MediaType mediaTypes )
  165. {
  166. #if !UNITY_EDITOR && UNITY_ANDROID
  167. object threadLock = new object();
  168. lock( threadLock )
  169. {
  170. NGPermissionCallbackAndroid nativeCallback = new NGPermissionCallbackAndroid( threadLock );
  171. AJC.CallStatic( "RequestPermission", Context, nativeCallback, permissionType == PermissionType.Read, (int) mediaTypes, PlayerPrefs.GetInt( "NativeGalleryPermission", (int) Permission.ShouldAsk ) );
  172. if( nativeCallback.Result == -1 )
  173. System.Threading.Monitor.Wait( threadLock );
  174. if( (Permission) nativeCallback.Result != Permission.ShouldAsk && PlayerPrefs.GetInt( "NativeGalleryPermission", -1 ) != nativeCallback.Result )
  175. {
  176. PlayerPrefs.SetInt( "NativeGalleryPermission", nativeCallback.Result );
  177. PlayerPrefs.Save();
  178. }
  179. return (Permission) nativeCallback.Result;
  180. }
  181. #elif !UNITY_EDITOR && UNITY_IOS
  182. // result == 3: LimitedAccess permission on iOS, no need to handle it when PermissionFreeMode is set to true
  183. int result = _NativeGallery_RequestPermission( permissionType == PermissionType.Read ? 1 : 0, PermissionFreeMode ? 1 : 0 );
  184. return result == 3 ? Permission.Granted : (Permission) result;
  185. #else
  186. return Permission.Granted;
  187. #endif
  188. }
  189. // This function isn't needed when PermissionFreeMode is set to true
  190. private static void TryExtendLimitedAccessPermission()
  191. {
  192. if( IsMediaPickerBusy() )
  193. return;
  194. #if !UNITY_EDITOR && UNITY_IOS
  195. _NativeGallery_ShowLimitedLibraryPicker();
  196. #endif
  197. }
  198. public static bool CanOpenSettings()
  199. {
  200. #if !UNITY_EDITOR && UNITY_IOS
  201. return _NativeGallery_CanOpenSettings() == 1;
  202. #else
  203. return true;
  204. #endif
  205. }
  206. public static void OpenSettings()
  207. {
  208. #if !UNITY_EDITOR && UNITY_ANDROID
  209. AJC.CallStatic( "OpenSettings", Context );
  210. #elif !UNITY_EDITOR && UNITY_IOS
  211. _NativeGallery_OpenSettings();
  212. #endif
  213. }
  214. #endregion
  215. #region Save Functions
  216. public static Permission SaveImageToGallery( byte[] mediaBytes, string album, string filename, MediaSaveCallback callback = null )
  217. {
  218. return SaveToGallery( mediaBytes, album, filename, MediaType.Image, callback );
  219. }
  220. public static Permission SaveImageToGallery( string existingMediaPath, string album, string filename, MediaSaveCallback callback = null )
  221. {
  222. return SaveToGallery( existingMediaPath, album, filename, MediaType.Image, callback );
  223. }
  224. public static Permission SaveImageToGallery( Texture2D image, string album, string filename, MediaSaveCallback callback = null )
  225. {
  226. if( image == null )
  227. throw new ArgumentException( "Parameter 'image' is null!" );
  228. if( filename.EndsWith( ".jpeg", StringComparison.OrdinalIgnoreCase ) || filename.EndsWith( ".jpg", StringComparison.OrdinalIgnoreCase ) )
  229. return SaveToGallery( GetTextureBytes( image, true ), album, filename, MediaType.Image, callback );
  230. else if( filename.EndsWith( ".png", StringComparison.OrdinalIgnoreCase ) )
  231. return SaveToGallery( GetTextureBytes( image, false ), album, filename, MediaType.Image, callback );
  232. else
  233. return SaveToGallery( GetTextureBytes( image, false ), album, filename + ".png", MediaType.Image, callback );
  234. }
  235. public static Permission SaveVideoToGallery( byte[] mediaBytes, string album, string filename, MediaSaveCallback callback = null )
  236. {
  237. return SaveToGallery( mediaBytes, album, filename, MediaType.Video, callback );
  238. }
  239. public static Permission SaveVideoToGallery( string existingMediaPath, string album, string filename, MediaSaveCallback callback = null )
  240. {
  241. return SaveToGallery( existingMediaPath, album, filename, MediaType.Video, callback );
  242. }
  243. private static Permission SaveAudioToGallery( byte[] mediaBytes, string album, string filename, MediaSaveCallback callback = null )
  244. {
  245. return SaveToGallery( mediaBytes, album, filename, MediaType.Audio, callback );
  246. }
  247. private static Permission SaveAudioToGallery( string existingMediaPath, string album, string filename, MediaSaveCallback callback = null )
  248. {
  249. return SaveToGallery( existingMediaPath, album, filename, MediaType.Audio, callback );
  250. }
  251. #endregion
  252. #region Load Functions
  253. public static bool CanSelectMultipleFilesFromGallery()
  254. {
  255. #if !UNITY_EDITOR && UNITY_ANDROID
  256. return AJC.CallStatic<bool>( "CanSelectMultipleMedia" );
  257. #elif !UNITY_EDITOR && UNITY_IOS
  258. return _NativeGallery_CanPickMultipleMedia() == 1;
  259. #else
  260. return false;
  261. #endif
  262. }
  263. public static bool CanSelectMultipleMediaTypesFromGallery()
  264. {
  265. #if UNITY_EDITOR
  266. return true;
  267. #elif UNITY_ANDROID
  268. return AJC.CallStatic<bool>( "CanSelectMultipleMediaTypes" );
  269. #elif UNITY_IOS
  270. return true;
  271. #else
  272. return false;
  273. #endif
  274. }
  275. public static Permission GetImageFromGallery( MediaPickCallback callback, string title = "", string mime = "image/*" )
  276. {
  277. return GetMediaFromGallery( callback, MediaType.Image, mime, title );
  278. }
  279. public static Permission GetVideoFromGallery( MediaPickCallback callback, string title = "", string mime = "video/*" )
  280. {
  281. return GetMediaFromGallery( callback, MediaType.Video, mime, title );
  282. }
  283. public static Permission GetAudioFromGallery( MediaPickCallback callback, string title = "", string mime = "audio/*" )
  284. {
  285. return GetMediaFromGallery( callback, MediaType.Audio, mime, title );
  286. }
  287. public static Permission GetMixedMediaFromGallery( MediaPickCallback callback, MediaType mediaTypes, string title = "" )
  288. {
  289. return GetMediaFromGallery( callback, mediaTypes, "*/*", title );
  290. }
  291. public static Permission GetImagesFromGallery( MediaPickMultipleCallback callback, string title = "", string mime = "image/*" )
  292. {
  293. return GetMultipleMediaFromGallery( callback, MediaType.Image, mime, title );
  294. }
  295. public static Permission GetVideosFromGallery( MediaPickMultipleCallback callback, string title = "", string mime = "video/*" )
  296. {
  297. return GetMultipleMediaFromGallery( callback, MediaType.Video, mime, title );
  298. }
  299. public static Permission GetAudiosFromGallery( MediaPickMultipleCallback callback, string title = "", string mime = "audio/*" )
  300. {
  301. return GetMultipleMediaFromGallery( callback, MediaType.Audio, mime, title );
  302. }
  303. public static Permission GetMixedMediasFromGallery( MediaPickMultipleCallback callback, MediaType mediaTypes, string title = "" )
  304. {
  305. return GetMultipleMediaFromGallery( callback, mediaTypes, "*/*", title );
  306. }
  307. public static bool IsMediaPickerBusy()
  308. {
  309. #if !UNITY_EDITOR && UNITY_IOS
  310. return NGMediaReceiveCallbackiOS.IsBusy;
  311. #else
  312. return false;
  313. #endif
  314. }
  315. public static MediaType GetMediaTypeOfFile( string path )
  316. {
  317. if( string.IsNullOrEmpty( path ) )
  318. return (MediaType) 0;
  319. string extension = Path.GetExtension( path );
  320. if( string.IsNullOrEmpty( extension ) )
  321. return (MediaType) 0;
  322. if( extension[0] == '.' )
  323. {
  324. if( extension.Length == 1 )
  325. return (MediaType) 0;
  326. extension = extension.Substring( 1 );
  327. }
  328. #if UNITY_EDITOR
  329. extension = extension.ToLowerInvariant();
  330. if( extension == "png" || extension == "jpg" || extension == "jpeg" || extension == "gif" || extension == "bmp" || extension == "tiff" )
  331. return MediaType.Image;
  332. else if( extension == "mp4" || extension == "mov" || extension == "wav" || extension == "avi" )
  333. return MediaType.Video;
  334. else if( extension == "mp3" || extension == "aac" || extension == "flac" )
  335. return MediaType.Audio;
  336. return (MediaType) 0;
  337. #elif UNITY_ANDROID
  338. string mime = AJC.CallStatic<string>( "GetMimeTypeFromExtension", extension.ToLowerInvariant() );
  339. if( string.IsNullOrEmpty( mime ) )
  340. return (MediaType) 0;
  341. else if( mime.StartsWith( "image/" ) )
  342. return MediaType.Image;
  343. else if( mime.StartsWith( "video/" ) )
  344. return MediaType.Video;
  345. else if( mime.StartsWith( "audio/" ) )
  346. return MediaType.Audio;
  347. else
  348. return (MediaType) 0;
  349. #elif UNITY_IOS
  350. return (MediaType) _NativeGallery_GetMediaTypeFromExtension( extension.ToLowerInvariant() );
  351. #else
  352. return (MediaType) 0;
  353. #endif
  354. }
  355. #endregion
  356. #region Internal Functions
  357. private static Permission SaveToGallery( byte[] mediaBytes, string album, string filename, MediaType mediaType, MediaSaveCallback callback )
  358. {
  359. Permission result = RequestPermission( PermissionType.Write, mediaType );
  360. if( result == Permission.Granted )
  361. {
  362. if( mediaBytes == null || mediaBytes.Length == 0 )
  363. throw new ArgumentException( "Parameter 'mediaBytes' is null or empty!" );
  364. if( album == null || album.Length == 0 )
  365. throw new ArgumentException( "Parameter 'album' is null or empty!" );
  366. if( filename == null || filename.Length == 0 )
  367. throw new ArgumentException( "Parameter 'filename' is null or empty!" );
  368. if( string.IsNullOrEmpty( Path.GetExtension( filename ) ) )
  369. Debug.LogWarning( "'filename' doesn't have an extension, this might result in unexpected behaviour!" );
  370. string path = GetTemporarySavePath( filename );
  371. #if UNITY_EDITOR
  372. Debug.Log( "SaveToGallery called successfully in the Editor" );
  373. #else
  374. File.WriteAllBytes( path, mediaBytes );
  375. #endif
  376. SaveToGalleryInternal( path, album, mediaType, callback );
  377. }
  378. return result;
  379. }
  380. private static Permission SaveToGallery( string existingMediaPath, string album, string filename, MediaType mediaType, MediaSaveCallback callback )
  381. {
  382. Permission result = RequestPermission( PermissionType.Write, mediaType );
  383. if( result == Permission.Granted )
  384. {
  385. if( !File.Exists( existingMediaPath ) )
  386. throw new FileNotFoundException( "File not found at " + existingMediaPath );
  387. if( album == null || album.Length == 0 )
  388. throw new ArgumentException( "Parameter 'album' is null or empty!" );
  389. if( filename == null || filename.Length == 0 )
  390. throw new ArgumentException( "Parameter 'filename' is null or empty!" );
  391. if( string.IsNullOrEmpty( Path.GetExtension( filename ) ) )
  392. {
  393. string originalExtension = Path.GetExtension( existingMediaPath );
  394. if( string.IsNullOrEmpty( originalExtension ) )
  395. Debug.LogWarning( "'filename' doesn't have an extension, this might result in unexpected behaviour!" );
  396. else
  397. filename += originalExtension;
  398. }
  399. string path = GetTemporarySavePath( filename );
  400. #if UNITY_EDITOR
  401. Debug.Log( "SaveToGallery called successfully in the Editor" );
  402. #else
  403. File.Copy( existingMediaPath, path, true );
  404. #endif
  405. SaveToGalleryInternal( path, album, mediaType, callback );
  406. }
  407. return result;
  408. }
  409. private static void SaveToGalleryInternal( string path, string album, MediaType mediaType, MediaSaveCallback callback )
  410. {
  411. #if !UNITY_EDITOR && UNITY_ANDROID
  412. string savePath = AJC.CallStatic<string>( "SaveMedia", Context, (int) mediaType, path, album );
  413. File.Delete( path );
  414. if( callback != null )
  415. callback( !string.IsNullOrEmpty( savePath ), savePath );
  416. #elif !UNITY_EDITOR && UNITY_IOS
  417. if( mediaType == MediaType.Audio )
  418. {
  419. Debug.LogError( "Saving audio files is not supported on iOS" );
  420. if( callback != null )
  421. callback( false, null );
  422. return;
  423. }
  424. Debug.Log( "Saving to Pictures: " + Path.GetFileName( path ) );
  425. NGMediaSaveCallbackiOS.Initialize( callback );
  426. if( mediaType == MediaType.Image )
  427. _NativeGallery_ImageWriteToAlbum( path, album, PermissionFreeMode ? 1 : 0 );
  428. else if( mediaType == MediaType.Video )
  429. _NativeGallery_VideoWriteToAlbum( path, album, PermissionFreeMode ? 1 : 0 );
  430. #else
  431. if( callback != null )
  432. callback( true, null );
  433. #endif
  434. }
  435. private static string GetTemporarySavePath( string filename )
  436. {
  437. string saveDir = Path.Combine( Application.persistentDataPath, "NGallery" );
  438. Directory.CreateDirectory( saveDir );
  439. #if !UNITY_EDITOR && UNITY_IOS
  440. // Ensure a unique temporary filename on iOS:
  441. // iOS internally copies images/videos to Photos directory of the system,
  442. // but the process is async. The redundant file is deleted by objective-c code
  443. // automatically after the media is saved but while it is being saved, the file
  444. // should NOT be overwritten. Therefore, always ensure a unique filename on iOS
  445. string path = Path.Combine( saveDir, filename );
  446. if( File.Exists( path ) )
  447. {
  448. int fileIndex = 0;
  449. string filenameWithoutExtension = Path.GetFileNameWithoutExtension( filename );
  450. string extension = Path.GetExtension( filename );
  451. do
  452. {
  453. path = Path.Combine( saveDir, string.Concat( filenameWithoutExtension, ++fileIndex, extension ) );
  454. } while( File.Exists( path ) );
  455. }
  456. return path;
  457. #else
  458. return Path.Combine( saveDir, filename );
  459. #endif
  460. }
  461. private static Permission GetMediaFromGallery( MediaPickCallback callback, MediaType mediaType, string mime, string title )
  462. {
  463. Permission result = RequestPermission( PermissionType.Read, mediaType );
  464. if( result == Permission.Granted && !IsMediaPickerBusy() )
  465. {
  466. #if UNITY_EDITOR
  467. System.Collections.Generic.List<string> editorFilters = new System.Collections.Generic.List<string>( 4 );
  468. if( ( mediaType & MediaType.Image ) == MediaType.Image )
  469. {
  470. editorFilters.Add( "Image files" );
  471. editorFilters.Add( "png,jpg,jpeg" );
  472. }
  473. if( ( mediaType & MediaType.Video ) == MediaType.Video )
  474. {
  475. editorFilters.Add( "Video files" );
  476. editorFilters.Add( "mp4,mov,wav,avi" );
  477. }
  478. if( ( mediaType & MediaType.Audio ) == MediaType.Audio )
  479. {
  480. editorFilters.Add( "Audio files" );
  481. editorFilters.Add( "mp3,aac,flac" );
  482. }
  483. editorFilters.Add( "All files" );
  484. editorFilters.Add( "*" );
  485. string pickedFile = UnityEditor.EditorUtility.OpenFilePanelWithFilters( "Select file", "", editorFilters.ToArray() );
  486. if( callback != null )
  487. callback( pickedFile != "" ? pickedFile : null );
  488. #elif UNITY_ANDROID
  489. AJC.CallStatic( "PickMedia", Context, new NGMediaReceiveCallbackAndroid( callback, null ), (int) mediaType, false, SelectedMediaPath, mime, title );
  490. #elif UNITY_IOS
  491. if( mediaType == MediaType.Audio )
  492. {
  493. Debug.LogError( "Picking audio files is not supported on iOS" );
  494. if( callback != null ) // Selecting audio files is not supported on iOS
  495. callback( null );
  496. }
  497. else
  498. {
  499. NGMediaReceiveCallbackiOS.Initialize( callback, null );
  500. _NativeGallery_PickMedia( SelectedMediaPath, (int) ( mediaType & ~MediaType.Audio ), PermissionFreeMode ? 1 : 0, 1 );
  501. }
  502. #else
  503. if( callback != null )
  504. callback( null );
  505. #endif
  506. }
  507. return result;
  508. }
  509. private static Permission GetMultipleMediaFromGallery( MediaPickMultipleCallback callback, MediaType mediaType, string mime, string title )
  510. {
  511. Permission result = RequestPermission( PermissionType.Read, mediaType );
  512. if( result == Permission.Granted && !IsMediaPickerBusy() )
  513. {
  514. if( CanSelectMultipleFilesFromGallery() )
  515. {
  516. #if !UNITY_EDITOR && UNITY_ANDROID
  517. AJC.CallStatic( "PickMedia", Context, new NGMediaReceiveCallbackAndroid( null, callback ), (int) mediaType, true, SelectedMediaPath, mime, title );
  518. #elif !UNITY_EDITOR && UNITY_IOS
  519. if( mediaType == MediaType.Audio )
  520. {
  521. Debug.LogError( "Picking audio files is not supported on iOS" );
  522. if( callback != null ) // Selecting audio files is not supported on iOS
  523. callback( null );
  524. }
  525. else
  526. {
  527. NGMediaReceiveCallbackiOS.Initialize( null, callback );
  528. _NativeGallery_PickMedia( SelectedMediaPath, (int) ( mediaType & ~MediaType.Audio ), PermissionFreeMode ? 1 : 0, 0 );
  529. }
  530. #else
  531. if( callback != null )
  532. callback( null );
  533. #endif
  534. }
  535. else if( callback != null )
  536. callback( null );
  537. }
  538. return result;
  539. }
  540. private static byte[] GetTextureBytes( Texture2D texture, bool isJpeg )
  541. {
  542. try
  543. {
  544. return isJpeg ? texture.EncodeToJPG( 100 ) : texture.EncodeToPNG();
  545. }
  546. catch( UnityException )
  547. {
  548. return GetTextureBytesFromCopy( texture, isJpeg );
  549. }
  550. catch( ArgumentException )
  551. {
  552. return GetTextureBytesFromCopy( texture, isJpeg );
  553. }
  554. #pragma warning disable 0162
  555. return null;
  556. #pragma warning restore 0162
  557. }
  558. private static byte[] GetTextureBytesFromCopy( Texture2D texture, bool isJpeg )
  559. {
  560. // Texture is marked as non-readable, create a readable copy and save it instead
  561. Debug.LogWarning( "Saving non-readable textures is slower than saving readable textures" );
  562. Texture2D sourceTexReadable = null;
  563. RenderTexture rt = RenderTexture.GetTemporary( texture.width, texture.height );
  564. RenderTexture activeRT = RenderTexture.active;
  565. try
  566. {
  567. Graphics.Blit( texture, rt );
  568. RenderTexture.active = rt;
  569. sourceTexReadable = new Texture2D( texture.width, texture.height, isJpeg ? TextureFormat.RGB24 : TextureFormat.RGBA32, false );
  570. sourceTexReadable.ReadPixels( new Rect( 0, 0, texture.width, texture.height ), 0, 0, false );
  571. sourceTexReadable.Apply( false, false );
  572. }
  573. catch( Exception e )
  574. {
  575. Debug.LogException( e );
  576. Object.DestroyImmediate( sourceTexReadable );
  577. return null;
  578. }
  579. finally
  580. {
  581. RenderTexture.active = activeRT;
  582. RenderTexture.ReleaseTemporary( rt );
  583. }
  584. try
  585. {
  586. return isJpeg ? sourceTexReadable.EncodeToJPG( 100 ) : sourceTexReadable.EncodeToPNG();
  587. }
  588. catch( Exception e )
  589. {
  590. Debug.LogException( e );
  591. return null;
  592. }
  593. finally
  594. {
  595. Object.DestroyImmediate( sourceTexReadable );
  596. }
  597. }
  598. #if UNITY_2018_4_OR_NEWER && !NATIVE_GALLERY_DISABLE_ASYNC_FUNCTIONS
  599. private static async Task<T> TryCallNativeAndroidFunctionOnSeparateThread<T>( Func<T> function )
  600. {
  601. T result = default( T );
  602. bool hasResult = false;
  603. await Task.Run( () =>
  604. {
  605. if( AndroidJNI.AttachCurrentThread() != 0 )
  606. Debug.LogWarning( "Couldn't attach JNI thread, calling native function on the main thread" );
  607. else
  608. {
  609. try
  610. {
  611. result = function();
  612. hasResult = true;
  613. }
  614. finally
  615. {
  616. AndroidJNI.DetachCurrentThread();
  617. }
  618. }
  619. } );
  620. return hasResult ? result : function();
  621. }
  622. #endif
  623. #endregion
  624. #region Utility Functions
  625. public static Texture2D LoadImageAtPath( string imagePath, int maxSize = -1, bool markTextureNonReadable = true, bool generateMipmaps = true, bool linearColorSpace = false )
  626. {
  627. if( string.IsNullOrEmpty( imagePath ) )
  628. throw new ArgumentException( "Parameter 'imagePath' is null or empty!" );
  629. if( !File.Exists( imagePath ) )
  630. throw new FileNotFoundException( "File not found at " + imagePath );
  631. if( maxSize <= 0 )
  632. maxSize = SystemInfo.maxTextureSize;
  633. #if !UNITY_EDITOR && UNITY_ANDROID
  634. string loadPath = AJC.CallStatic<string>( "LoadImageAtPath", Context, imagePath, TemporaryImagePath, maxSize );
  635. #elif !UNITY_EDITOR && UNITY_IOS
  636. string loadPath = _NativeGallery_LoadImageAtPath( imagePath, TemporaryImagePath, maxSize );
  637. #else
  638. string loadPath = imagePath;
  639. #endif
  640. string extension = Path.GetExtension( imagePath ).ToLowerInvariant();
  641. TextureFormat format = ( extension == ".jpg" || extension == ".jpeg" ) ? TextureFormat.RGB24 : TextureFormat.RGBA32;
  642. Texture2D result = new Texture2D( 2, 2, format, generateMipmaps, linearColorSpace );
  643. try
  644. {
  645. if( !result.LoadImage( File.ReadAllBytes( loadPath ), markTextureNonReadable ) )
  646. {
  647. Debug.LogWarning( "Couldn't load image at path: " + loadPath );
  648. Object.DestroyImmediate( result );
  649. return null;
  650. }
  651. }
  652. catch( Exception e )
  653. {
  654. Debug.LogException( e );
  655. Object.DestroyImmediate( result );
  656. return null;
  657. }
  658. finally
  659. {
  660. if( loadPath != imagePath )
  661. {
  662. try
  663. {
  664. File.Delete( loadPath );
  665. }
  666. catch { }
  667. }
  668. }
  669. return result;
  670. }
  671. #if UNITY_2018_4_OR_NEWER && !NATIVE_GALLERY_DISABLE_ASYNC_FUNCTIONS
  672. public static async Task<Texture2D> LoadImageAtPathAsync( string imagePath, int maxSize = -1, bool markTextureNonReadable = true, bool generateMipmaps = true, bool linearColorSpace = false )
  673. {
  674. if( string.IsNullOrEmpty( imagePath ) )
  675. throw new ArgumentException( "Parameter 'imagePath' is null or empty!" );
  676. if( !File.Exists( imagePath ) )
  677. throw new FileNotFoundException( "File not found at " + imagePath );
  678. if( maxSize <= 0 )
  679. maxSize = SystemInfo.maxTextureSize;
  680. #if !UNITY_EDITOR && UNITY_ANDROID
  681. string temporaryImagePath = TemporaryImagePath; // Must be accessed from main thread
  682. string loadPath = await TryCallNativeAndroidFunctionOnSeparateThread( () => AJC.CallStatic<string>( "LoadImageAtPath", Context, imagePath, temporaryImagePath, maxSize ) );
  683. #elif !UNITY_EDITOR && UNITY_IOS
  684. string temporaryImagePath = TemporaryImagePath; // Must be accessed from main thread
  685. string loadPath = await Task.Run( () => _NativeGallery_LoadImageAtPath( imagePath, temporaryImagePath, maxSize ) );
  686. #else
  687. string loadPath = imagePath;
  688. #endif
  689. Texture2D result = null;
  690. if( !linearColorSpace )
  691. {
  692. using( UnityWebRequest www = UnityWebRequestTexture.GetTexture( "file://" + loadPath, markTextureNonReadable && !generateMipmaps ) )
  693. {
  694. UnityWebRequestAsyncOperation asyncOperation = www.SendWebRequest();
  695. while( !asyncOperation.isDone )
  696. await Task.Yield();
  697. #if UNITY_2020_1_OR_NEWER
  698. if( www.result != UnityWebRequest.Result.Success )
  699. #else
  700. if( www.isNetworkError || www.isHttpError )
  701. #endif
  702. {
  703. Debug.LogWarning( "Couldn't use UnityWebRequest to load image, falling back to LoadImage: " + www.error );
  704. }
  705. else
  706. {
  707. Texture2D texture = DownloadHandlerTexture.GetContent( www );
  708. if( !generateMipmaps )
  709. result = texture;
  710. else
  711. {
  712. Texture2D mipmapTexture = null;
  713. try
  714. {
  715. // Generate a Texture with mipmaps enabled
  716. // Credits: https://forum.unity.com/threads/generate-mipmaps-at-runtime-for-a-texture-loaded-with-unitywebrequest.644842/#post-7571809
  717. NativeArray<byte> textureData = texture.GetRawTextureData<byte>();
  718. mipmapTexture = new Texture2D( texture.width, texture.height, texture.format, true );
  719. #if UNITY_2019_3_OR_NEWER
  720. mipmapTexture.SetPixelData( textureData, 0 );
  721. #else
  722. NativeArray<byte> mipmapTextureData = mipmapTexture.GetRawTextureData<byte>();
  723. NativeArray<byte>.Copy( textureData, mipmapTextureData, textureData.Length );
  724. mipmapTexture.LoadRawTextureData( mipmapTextureData );
  725. #endif
  726. mipmapTexture.Apply( true, markTextureNonReadable );
  727. result = mipmapTexture;
  728. }
  729. catch( Exception e )
  730. {
  731. Debug.LogException( e );
  732. if( mipmapTexture )
  733. Object.DestroyImmediate( mipmapTexture );
  734. }
  735. finally
  736. {
  737. Object.DestroyImmediate( texture );
  738. }
  739. }
  740. }
  741. }
  742. }
  743. if( !result ) // Fallback to Texture2D.LoadImage if something goes wrong
  744. {
  745. string extension = Path.GetExtension( imagePath ).ToLowerInvariant();
  746. TextureFormat format = ( extension == ".jpg" || extension == ".jpeg" ) ? TextureFormat.RGB24 : TextureFormat.RGBA32;
  747. result = new Texture2D( 2, 2, format, generateMipmaps, linearColorSpace );
  748. try
  749. {
  750. if( !result.LoadImage( File.ReadAllBytes( loadPath ), markTextureNonReadable ) )
  751. {
  752. Debug.LogWarning( "Couldn't load image at path: " + loadPath );
  753. Object.DestroyImmediate( result );
  754. return null;
  755. }
  756. }
  757. catch( Exception e )
  758. {
  759. Debug.LogException( e );
  760. Object.DestroyImmediate( result );
  761. return null;
  762. }
  763. finally
  764. {
  765. if( loadPath != imagePath )
  766. {
  767. try
  768. {
  769. File.Delete( loadPath );
  770. }
  771. catch { }
  772. }
  773. }
  774. }
  775. return result;
  776. }
  777. #endif
  778. public static Texture2D GetVideoThumbnail( string videoPath, int maxSize = -1, double captureTimeInSeconds = -1.0, bool markTextureNonReadable = true, bool generateMipmaps = true, bool linearColorSpace = false )
  779. {
  780. if( maxSize <= 0 )
  781. maxSize = SystemInfo.maxTextureSize;
  782. #if !UNITY_EDITOR && UNITY_ANDROID
  783. string thumbnailPath = AJC.CallStatic<string>( "GetVideoThumbnail", Context, videoPath, TemporaryImagePath + ".png", false, maxSize, captureTimeInSeconds );
  784. #elif !UNITY_EDITOR && UNITY_IOS
  785. string thumbnailPath = _NativeGallery_GetVideoThumbnail( videoPath, TemporaryImagePath + ".png", maxSize, captureTimeInSeconds );
  786. #else
  787. string thumbnailPath = null;
  788. #endif
  789. if( !string.IsNullOrEmpty( thumbnailPath ) )
  790. return LoadImageAtPath( thumbnailPath, maxSize, markTextureNonReadable, generateMipmaps, linearColorSpace );
  791. else
  792. return null;
  793. }
  794. #if UNITY_2018_4_OR_NEWER && !NATIVE_GALLERY_DISABLE_ASYNC_FUNCTIONS
  795. public static async Task<Texture2D> GetVideoThumbnailAsync( string videoPath, int maxSize = -1, double captureTimeInSeconds = -1.0, bool markTextureNonReadable = true, bool generateMipmaps = true, bool linearColorSpace = false )
  796. {
  797. if( maxSize <= 0 )
  798. maxSize = SystemInfo.maxTextureSize;
  799. #if !UNITY_EDITOR && UNITY_ANDROID
  800. string temporaryImagePath = TemporaryImagePath; // Must be accessed from main thread
  801. string thumbnailPath = await TryCallNativeAndroidFunctionOnSeparateThread( () => AJC.CallStatic<string>( "GetVideoThumbnail", Context, videoPath, temporaryImagePath + ".png", false, maxSize, captureTimeInSeconds ) );
  802. #elif !UNITY_EDITOR && UNITY_IOS
  803. string temporaryImagePath = TemporaryImagePath; // Must be accessed from main thread
  804. string thumbnailPath = await Task.Run( () => _NativeGallery_GetVideoThumbnail( videoPath, temporaryImagePath + ".png", maxSize, captureTimeInSeconds ) );
  805. #else
  806. string thumbnailPath = null;
  807. #endif
  808. if( !string.IsNullOrEmpty( thumbnailPath ) )
  809. return await LoadImageAtPathAsync( thumbnailPath, maxSize, markTextureNonReadable, generateMipmaps, linearColorSpace );
  810. else
  811. return null;
  812. }
  813. #endif
  814. public static ImageProperties GetImageProperties( string imagePath )
  815. {
  816. if( !File.Exists( imagePath ) )
  817. throw new FileNotFoundException( "File not found at " + imagePath );
  818. #if !UNITY_EDITOR && UNITY_ANDROID
  819. string value = AJC.CallStatic<string>( "GetImageProperties", Context, imagePath );
  820. #elif !UNITY_EDITOR && UNITY_IOS
  821. string value = _NativeGallery_GetImageProperties( imagePath );
  822. #else
  823. string value = null;
  824. #endif
  825. int width = 0, height = 0;
  826. string mimeType = null;
  827. ImageOrientation orientation = ImageOrientation.Unknown;
  828. if( !string.IsNullOrEmpty( value ) )
  829. {
  830. string[] properties = value.Split( '>' );
  831. if( properties != null && properties.Length >= 4 )
  832. {
  833. if( !int.TryParse( properties[0].Trim(), out width ) )
  834. width = 0;
  835. if( !int.TryParse( properties[1].Trim(), out height ) )
  836. height = 0;
  837. mimeType = properties[2].Trim();
  838. if( mimeType.Length == 0 )
  839. {
  840. string extension = Path.GetExtension( imagePath ).ToLowerInvariant();
  841. if( extension == ".png" )
  842. mimeType = "image/png";
  843. else if( extension == ".jpg" || extension == ".jpeg" )
  844. mimeType = "image/jpeg";
  845. else if( extension == ".gif" )
  846. mimeType = "image/gif";
  847. else if( extension == ".bmp" )
  848. mimeType = "image/bmp";
  849. else
  850. mimeType = null;
  851. }
  852. int orientationInt;
  853. if( int.TryParse( properties[3].Trim(), out orientationInt ) )
  854. orientation = (ImageOrientation) orientationInt;
  855. }
  856. }
  857. return new ImageProperties( width, height, mimeType, orientation );
  858. }
  859. public static VideoProperties GetVideoProperties( string videoPath )
  860. {
  861. if( !File.Exists( videoPath ) )
  862. throw new FileNotFoundException( "File not found at " + videoPath );
  863. #if !UNITY_EDITOR && UNITY_ANDROID
  864. string value = AJC.CallStatic<string>( "GetVideoProperties", Context, videoPath );
  865. #elif !UNITY_EDITOR && UNITY_IOS
  866. string value = _NativeGallery_GetVideoProperties( videoPath );
  867. #else
  868. string value = null;
  869. #endif
  870. int width = 0, height = 0;
  871. long duration = 0L;
  872. float rotation = 0f;
  873. if( !string.IsNullOrEmpty( value ) )
  874. {
  875. string[] properties = value.Split( '>' );
  876. if( properties != null && properties.Length >= 4 )
  877. {
  878. if( !int.TryParse( properties[0].Trim(), out width ) )
  879. width = 0;
  880. if( !int.TryParse( properties[1].Trim(), out height ) )
  881. height = 0;
  882. if( !long.TryParse( properties[2].Trim(), out duration ) )
  883. duration = 0L;
  884. if( !float.TryParse( properties[3].Trim().Replace( ',', '.' ), NumberStyles.Float, CultureInfo.InvariantCulture, out rotation ) )
  885. rotation = 0f;
  886. }
  887. }
  888. if( rotation == -90f )
  889. rotation = 270f;
  890. return new VideoProperties( width, height, duration, rotation );
  891. }
  892. #endregion
  893. }