No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

NativeGallery.cs 34KB

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