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 32KB

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