| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039 | using System;using System.Globalization;using System.IO;using UnityEngine;#if UNITY_2018_4_OR_NEWER && !NATIVE_GALLERY_DISABLE_ASYNC_FUNCTIONSusing System.Threading.Tasks;using Unity.Collections;using UnityEngine.Networking;#endif#if UNITY_ANDROID || UNITY_IOSusing NativeGalleryNamespace;#endifusing Object = UnityEngine.Object;public static class NativeGallery{	public struct ImageProperties	{		public readonly int width;		public readonly int height;		public readonly string mimeType;		public readonly ImageOrientation orientation;		public ImageProperties( int width, int height, string mimeType, ImageOrientation orientation )		{			this.width = width;			this.height = height;			this.mimeType = mimeType;			this.orientation = orientation;		}	}	public struct VideoProperties	{		public readonly int width;		public readonly int height;		public readonly long duration;		public readonly float rotation;		public VideoProperties( int width, int height, long duration, float rotation )		{			this.width = width;			this.height = height;			this.duration = duration;			this.rotation = rotation;		}	}	public enum PermissionType { Read = 0, Write = 1 };	public enum Permission { Denied = 0, Granted = 1, ShouldAsk = 2 };	[Flags]	public enum MediaType { Image = 1, Video = 2, Audio = 4 };	// EXIF orientation: http://sylvana.net/jpegcrop/exif_orientation.html (indices are reordered)	public enum ImageOrientation { Unknown = -1, Normal = 0, Rotate90 = 1, Rotate180 = 2, Rotate270 = 3, FlipHorizontal = 4, Transpose = 5, FlipVertical = 6, Transverse = 7 };	public delegate void PermissionCallback( Permission permission );	public delegate void MediaSaveCallback( bool success, string path );	public delegate void MediaPickCallback( string path );	public delegate void MediaPickMultipleCallback( string[] paths );	#region Platform Specific Elements#if !UNITY_EDITOR && UNITY_ANDROID	private static AndroidJavaClass m_ajc = null;	private static AndroidJavaClass AJC	{		get		{			if( m_ajc == null )				m_ajc = new AndroidJavaClass( "com.yasirkula.unity.NativeGallery" );			return m_ajc;		}	}	private static AndroidJavaObject m_context = null;	private static AndroidJavaObject Context	{		get		{			if( m_context == null )			{				using( AndroidJavaObject unityClass = new AndroidJavaClass( "com.unity3d.player.UnityPlayer" ) )				{					m_context = unityClass.GetStatic<AndroidJavaObject>( "currentActivity" );				}			}			return m_context;		}	}#elif !UNITY_EDITOR && UNITY_IOS	[System.Runtime.InteropServices.DllImport( "__Internal" )]	private static extern int _NativeGallery_CheckPermission( int readPermission, int permissionFreeMode );	[System.Runtime.InteropServices.DllImport( "__Internal" )]	private static extern int _NativeGallery_RequestPermission( int readPermission, int permissionFreeMode, int asyncMode );	[System.Runtime.InteropServices.DllImport( "__Internal" )]	private static extern void _NativeGallery_ShowLimitedLibraryPicker();	[System.Runtime.InteropServices.DllImport( "__Internal" )]	private static extern int _NativeGallery_CanOpenSettings();	[System.Runtime.InteropServices.DllImport( "__Internal" )]	private static extern void _NativeGallery_OpenSettings();	[System.Runtime.InteropServices.DllImport( "__Internal" )]	private static extern int _NativeGallery_CanPickMultipleMedia();	[System.Runtime.InteropServices.DllImport( "__Internal" )]	private static extern int _NativeGallery_GetMediaTypeFromExtension( string extension );	[System.Runtime.InteropServices.DllImport( "__Internal" )]	private static extern void _NativeGallery_ImageWriteToAlbum( string path, string album, int permissionFreeMode );	[System.Runtime.InteropServices.DllImport( "__Internal" )]	private static extern void _NativeGallery_VideoWriteToAlbum( string path, string album, int permissionFreeMode );	[System.Runtime.InteropServices.DllImport( "__Internal" )]	private static extern void _NativeGallery_PickMedia( string mediaSavePath, int mediaType, int permissionFreeMode, int selectionLimit );	[System.Runtime.InteropServices.DllImport( "__Internal" )]	private static extern string _NativeGallery_GetImageProperties( string path );	[System.Runtime.InteropServices.DllImport( "__Internal" )]	private static extern string _NativeGallery_GetVideoProperties( string path );	[System.Runtime.InteropServices.DllImport( "__Internal" )]	private static extern string _NativeGallery_GetVideoThumbnail( string path, string thumbnailSavePath, int maxSize, double captureTimeInSeconds );	[System.Runtime.InteropServices.DllImport( "__Internal" )]	private static extern string _NativeGallery_LoadImageAtPath( string path, string temporaryFilePath, int maxSize );#endif#if !UNITY_EDITOR && ( UNITY_ANDROID || UNITY_IOS )	private static string m_temporaryImagePath = null;	private static string TemporaryImagePath	{		get		{			if( m_temporaryImagePath == null )			{				m_temporaryImagePath = Path.Combine( Application.temporaryCachePath, "tmpImg" );				Directory.CreateDirectory( Application.temporaryCachePath );			}			return m_temporaryImagePath;		}	}	private static string m_selectedMediaPath = null;	private static string SelectedMediaPath	{		get		{			if( m_selectedMediaPath == null )			{				m_selectedMediaPath = Path.Combine( Application.temporaryCachePath, "pickedMedia" );				Directory.CreateDirectory( Application.temporaryCachePath );			}			return m_selectedMediaPath;		}	}#endif	#endregion	#region Runtime Permissions	// 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.	// These issues are:	// - 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	//   this is caused by how permissions are handled synchronously in NativeGallery)	// - While saving images/videos to Photos, iOS 14+ users would see the "Select Photos" option (which is irrelevant in this context, hence confusing) and	//   the user must grant full Photos access in order to save the image/video to a custom album	// 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	// provided custom album	private const bool PermissionFreeMode = true;	public static Permission CheckPermission( PermissionType permissionType, MediaType mediaTypes )	{#if !UNITY_EDITOR && UNITY_ANDROID		Permission result = (Permission) AJC.CallStatic<int>( "CheckPermission", Context, permissionType == PermissionType.Read, (int) mediaTypes );		if( result == Permission.Denied && (Permission) PlayerPrefs.GetInt( "NativeGalleryPermission", (int) Permission.ShouldAsk ) == Permission.ShouldAsk )			result = Permission.ShouldAsk;		return result;#elif !UNITY_EDITOR && UNITY_IOS		return ProcessPermission( (Permission) _NativeGallery_CheckPermission( permissionType == PermissionType.Read ? 1 : 0, PermissionFreeMode ? 1 : 0 ) );#else		return Permission.Granted;#endif	}	public static Permission RequestPermission( PermissionType permissionType, MediaType mediaTypes )	{		// Don't block the main thread if the permission is already granted		if( CheckPermission( permissionType, mediaTypes ) == Permission.Granted )			return Permission.Granted;#if !UNITY_EDITOR && UNITY_ANDROID		object threadLock = new object();		lock( threadLock )		{			NGPermissionCallbackAndroid nativeCallback = new NGPermissionCallbackAndroid( threadLock );			AJC.CallStatic( "RequestPermission", Context, nativeCallback, permissionType == PermissionType.Read, (int) mediaTypes, (int) Permission.ShouldAsk );			if( nativeCallback.Result == -1 )				System.Threading.Monitor.Wait( threadLock );			if( (Permission) nativeCallback.Result != Permission.ShouldAsk && PlayerPrefs.GetInt( "NativeGalleryPermission", -1 ) != nativeCallback.Result )			{				PlayerPrefs.SetInt( "NativeGalleryPermission", nativeCallback.Result );				PlayerPrefs.Save();			}			return (Permission) nativeCallback.Result;		}#elif !UNITY_EDITOR && UNITY_IOS		return ProcessPermission( (Permission) _NativeGallery_RequestPermission( permissionType == PermissionType.Read ? 1 : 0, PermissionFreeMode ? 1 : 0, 0 ) );#else		return Permission.Granted;#endif	}	public static void RequestPermissionAsync( PermissionCallback callback, PermissionType permissionType, MediaType mediaTypes )	{#if !UNITY_EDITOR && UNITY_ANDROID		NGPermissionCallbackAsyncAndroid nativeCallback = new NGPermissionCallbackAsyncAndroid( callback );		AJC.CallStatic( "RequestPermission", Context, nativeCallback, permissionType == PermissionType.Read, (int) mediaTypes, (int) Permission.ShouldAsk );#elif !UNITY_EDITOR && UNITY_IOS		NGPermissionCallbackiOS.Initialize( ( result ) => callback( ProcessPermission( result ) ) );		_NativeGallery_RequestPermission( permissionType == PermissionType.Read ? 1 : 0, PermissionFreeMode ? 1 : 0, 1 );#else		callback( Permission.Granted );#endif	}#if UNITY_2018_4_OR_NEWER && !NATIVE_GALLERY_DISABLE_ASYNC_FUNCTIONS	public static Task<Permission> RequestPermissionAsync( PermissionType permissionType, MediaType mediaTypes )	{		TaskCompletionSource<Permission> tcs = new TaskCompletionSource<Permission>();		RequestPermissionAsync( ( permission ) => tcs.SetResult( permission ), permissionType, mediaTypes );		return tcs.Task;	}#endif	private static Permission ProcessPermission( Permission permission )	{		// result == 3: LimitedAccess permission on iOS, no need to handle it when PermissionFreeMode is set to true		return ( PermissionFreeMode && (int) permission == 3 ) ? Permission.Granted : permission;	}	// This function isn't needed when PermissionFreeMode is set to true	private static void TryExtendLimitedAccessPermission()	{		if( IsMediaPickerBusy() )			return;#if !UNITY_EDITOR && UNITY_IOS		_NativeGallery_ShowLimitedLibraryPicker();#endif	}	public static bool CanOpenSettings()	{#if !UNITY_EDITOR && UNITY_IOS		return _NativeGallery_CanOpenSettings() == 1;#else		return true;#endif	}	public static void OpenSettings()	{#if !UNITY_EDITOR && UNITY_ANDROID		AJC.CallStatic( "OpenSettings", Context );#elif !UNITY_EDITOR && UNITY_IOS		_NativeGallery_OpenSettings();#endif	}	#endregion	#region Save Functions	public static Permission SaveImageToGallery( byte[] mediaBytes, string album, string filename, MediaSaveCallback callback = null )	{		return SaveToGallery( mediaBytes, album, filename, MediaType.Image, callback );	}	public static Permission SaveImageToGallery( string existingMediaPath, string album, string filename, MediaSaveCallback callback = null )	{		return SaveToGallery( existingMediaPath, album, filename, MediaType.Image, callback );	}	public static Permission SaveImageToGallery( Texture2D image, string album, string filename, MediaSaveCallback callback = null )	{		if( image == null )			throw new ArgumentException( "Parameter 'image' is null!" );		if( filename.EndsWith( ".jpeg", StringComparison.OrdinalIgnoreCase ) || filename.EndsWith( ".jpg", StringComparison.OrdinalIgnoreCase ) )			return SaveToGallery( GetTextureBytes( image, true ), album, filename, MediaType.Image, callback );		else if( filename.EndsWith( ".png", StringComparison.OrdinalIgnoreCase ) )			return SaveToGallery( GetTextureBytes( image, false ), album, filename, MediaType.Image, callback );		else			return SaveToGallery( GetTextureBytes( image, false ), album, filename + ".png", MediaType.Image, callback );	}	public static Permission SaveVideoToGallery( byte[] mediaBytes, string album, string filename, MediaSaveCallback callback = null )	{		return SaveToGallery( mediaBytes, album, filename, MediaType.Video, callback );	}	public static Permission SaveVideoToGallery( string existingMediaPath, string album, string filename, MediaSaveCallback callback = null )	{		return SaveToGallery( existingMediaPath, album, filename, MediaType.Video, callback );	}	private static Permission SaveAudioToGallery( byte[] mediaBytes, string album, string filename, MediaSaveCallback callback = null )	{		return SaveToGallery( mediaBytes, album, filename, MediaType.Audio, callback );	}	private static Permission SaveAudioToGallery( string existingMediaPath, string album, string filename, MediaSaveCallback callback = null )	{		return SaveToGallery( existingMediaPath, album, filename, MediaType.Audio, callback );	}	#endregion	#region Load Functions	public static bool CanSelectMultipleFilesFromGallery()	{#if !UNITY_EDITOR && UNITY_ANDROID		return AJC.CallStatic<bool>( "CanSelectMultipleMedia" );#elif !UNITY_EDITOR && UNITY_IOS		return _NativeGallery_CanPickMultipleMedia() == 1;#else		return false;#endif	}	public static bool CanSelectMultipleMediaTypesFromGallery()	{#if UNITY_EDITOR		return true;#elif UNITY_ANDROID		return AJC.CallStatic<bool>( "CanSelectMultipleMediaTypes" );#elif UNITY_IOS		return true;#else		return false;#endif	}	public static Permission GetImageFromGallery( MediaPickCallback callback, string title = "", string mime = "image/*" )	{		return GetMediaFromGallery( callback, MediaType.Image, mime, title );	}	public static Permission GetVideoFromGallery( MediaPickCallback callback, string title = "", string mime = "video/*" )	{		return GetMediaFromGallery( callback, MediaType.Video, mime, title );	}	public static Permission GetAudioFromGallery( MediaPickCallback callback, string title = "", string mime = "audio/*" )	{		return GetMediaFromGallery( callback, MediaType.Audio, mime, title );	}	public static Permission GetMixedMediaFromGallery( MediaPickCallback callback, MediaType mediaTypes, string title = "" )	{		return GetMediaFromGallery( callback, mediaTypes, "*/*", title );	}	public static Permission GetImagesFromGallery( MediaPickMultipleCallback callback, string title = "", string mime = "image/*" )	{		return GetMultipleMediaFromGallery( callback, MediaType.Image, mime, title );	}	public static Permission GetVideosFromGallery( MediaPickMultipleCallback callback, string title = "", string mime = "video/*" )	{		return GetMultipleMediaFromGallery( callback, MediaType.Video, mime, title );	}	public static Permission GetAudiosFromGallery( MediaPickMultipleCallback callback, string title = "", string mime = "audio/*" )	{		return GetMultipleMediaFromGallery( callback, MediaType.Audio, mime, title );	}	public static Permission GetMixedMediasFromGallery( MediaPickMultipleCallback callback, MediaType mediaTypes, string title = "" )	{		return GetMultipleMediaFromGallery( callback, mediaTypes, "*/*", title );	}	public static bool IsMediaPickerBusy()	{#if !UNITY_EDITOR && UNITY_IOS		return NGMediaReceiveCallbackiOS.IsBusy;#else		return false;#endif	}	public static MediaType GetMediaTypeOfFile( string path )	{		if( string.IsNullOrEmpty( path ) )			return (MediaType) 0;		string extension = Path.GetExtension( path );		if( string.IsNullOrEmpty( extension ) )			return (MediaType) 0;		if( extension[0] == '.' )		{			if( extension.Length == 1 )				return (MediaType) 0;			extension = extension.Substring( 1 );		}#if UNITY_EDITOR		extension = extension.ToLowerInvariant();		if( extension == "png" || extension == "jpg" || extension == "jpeg" || extension == "gif" || extension == "bmp" || extension == "tiff" )			return MediaType.Image;		else if( extension == "mp4" || extension == "mov" || extension == "wav" || extension == "avi" )			return MediaType.Video;		else if( extension == "mp3" || extension == "aac" || extension == "flac" )			return MediaType.Audio;		return (MediaType) 0;#elif UNITY_ANDROID		string mime = AJC.CallStatic<string>( "GetMimeTypeFromExtension", extension.ToLowerInvariant() );		if( string.IsNullOrEmpty( mime ) )			return (MediaType) 0;		else if( mime.StartsWith( "image/" ) )			return MediaType.Image;		else if( mime.StartsWith( "video/" ) )			return MediaType.Video;		else if( mime.StartsWith( "audio/" ) )			return MediaType.Audio;		else			return (MediaType) 0;#elif UNITY_IOS		return (MediaType) _NativeGallery_GetMediaTypeFromExtension( extension.ToLowerInvariant() );#else		return (MediaType) 0;#endif	}	#endregion	#region Internal Functions	private static Permission SaveToGallery( byte[] mediaBytes, string album, string filename, MediaType mediaType, MediaSaveCallback callback )	{		Permission result = RequestPermission( PermissionType.Write, mediaType );		if( result == Permission.Granted )		{			if( mediaBytes == null || mediaBytes.Length == 0 )				throw new ArgumentException( "Parameter 'mediaBytes' is null or empty!" );			if( album == null || album.Length == 0 )				throw new ArgumentException( "Parameter 'album' is null or empty!" );			if( filename == null || filename.Length == 0 )				throw new ArgumentException( "Parameter 'filename' is null or empty!" );			if( string.IsNullOrEmpty( Path.GetExtension( filename ) ) )				Debug.LogWarning( "'filename' doesn't have an extension, this might result in unexpected behaviour!" );			string path = GetTemporarySavePath( filename );#if UNITY_EDITOR			Debug.Log( "SaveToGallery called successfully in the Editor" );#else			File.WriteAllBytes( path, mediaBytes );#endif			SaveToGalleryInternal( path, album, mediaType, callback );		}		return result;	}	private static Permission SaveToGallery( string existingMediaPath, string album, string filename, MediaType mediaType, MediaSaveCallback callback )	{		Permission result = RequestPermission( PermissionType.Write, mediaType );		if( result == Permission.Granted )		{			if( !File.Exists( existingMediaPath ) )				throw new FileNotFoundException( "File not found at " + existingMediaPath );			if( album == null || album.Length == 0 )				throw new ArgumentException( "Parameter 'album' is null or empty!" );			if( filename == null || filename.Length == 0 )				throw new ArgumentException( "Parameter 'filename' is null or empty!" );			if( string.IsNullOrEmpty( Path.GetExtension( filename ) ) )			{				string originalExtension = Path.GetExtension( existingMediaPath );				if( string.IsNullOrEmpty( originalExtension ) )					Debug.LogWarning( "'filename' doesn't have an extension, this might result in unexpected behaviour!" );				else					filename += originalExtension;			}			string path = GetTemporarySavePath( filename );#if UNITY_EDITOR			Debug.Log( "SaveToGallery called successfully in the Editor" );#else			File.Copy( existingMediaPath, path, true );#endif			SaveToGalleryInternal( path, album, mediaType, callback );		}		return result;	}	private static void SaveToGalleryInternal( string path, string album, MediaType mediaType, MediaSaveCallback callback )	{#if !UNITY_EDITOR && UNITY_ANDROID		string savePath = AJC.CallStatic<string>( "SaveMedia", Context, (int) mediaType, path, album );		File.Delete( path );		if( callback != null )			callback( !string.IsNullOrEmpty( savePath ), savePath );#elif !UNITY_EDITOR && UNITY_IOS		if( mediaType == MediaType.Audio )		{			Debug.LogError( "Saving audio files is not supported on iOS" );			if( callback != null )				callback( false, null );			return;		}		Debug.Log( "Saving to Pictures: " + Path.GetFileName( path ) );		NGMediaSaveCallbackiOS.Initialize( callback );		if( mediaType == MediaType.Image )			_NativeGallery_ImageWriteToAlbum( path, album, PermissionFreeMode ? 1 : 0 );		else if( mediaType == MediaType.Video )			_NativeGallery_VideoWriteToAlbum( path, album, PermissionFreeMode ? 1 : 0 );#else		if( callback != null )			callback( true, null );#endif	}	private static string GetTemporarySavePath( string filename )	{		string saveDir = Path.Combine( Application.persistentDataPath, "NGallery" );		Directory.CreateDirectory( saveDir );#if !UNITY_EDITOR && UNITY_IOS		// Ensure a unique temporary filename on iOS:		// iOS internally copies images/videos to Photos directory of the system,		// but the process is async. The redundant file is deleted by objective-c code		// automatically after the media is saved but while it is being saved, the file		// should NOT be overwritten. Therefore, always ensure a unique filename on iOS		string path = Path.Combine( saveDir, filename );		if( File.Exists( path ) )		{			int fileIndex = 0;			string filenameWithoutExtension = Path.GetFileNameWithoutExtension( filename );			string extension = Path.GetExtension( filename );			do			{				path = Path.Combine( saveDir, string.Concat( filenameWithoutExtension, ++fileIndex, extension ) );			} while( File.Exists( path ) );		}		return path;#else		return Path.Combine( saveDir, filename );#endif	}	private static Permission GetMediaFromGallery( MediaPickCallback callback, MediaType mediaType, string mime, string title )	{		Permission result = RequestPermission( PermissionType.Read, mediaType );		if( result == Permission.Granted && !IsMediaPickerBusy() )		{#if UNITY_EDITOR			System.Collections.Generic.List<string> editorFilters = new System.Collections.Generic.List<string>( 4 );			if( ( mediaType & MediaType.Image ) == MediaType.Image )			{				editorFilters.Add( "Image files" );				editorFilters.Add( "png,jpg,jpeg" );			}			if( ( mediaType & MediaType.Video ) == MediaType.Video )			{				editorFilters.Add( "Video files" );				editorFilters.Add( "mp4,mov,wav,avi" );			}			if( ( mediaType & MediaType.Audio ) == MediaType.Audio )			{				editorFilters.Add( "Audio files" );				editorFilters.Add( "mp3,aac,flac" );			}			editorFilters.Add( "All files" );			editorFilters.Add( "*" );			string pickedFile = UnityEditor.EditorUtility.OpenFilePanelWithFilters( "Select file", "", editorFilters.ToArray() );			if( callback != null )				callback( pickedFile != "" ? pickedFile : null );#elif UNITY_ANDROID			AJC.CallStatic( "PickMedia", Context, new NGMediaReceiveCallbackAndroid( callback, null ), (int) mediaType, false, SelectedMediaPath, mime, title );#elif UNITY_IOS			if( mediaType == MediaType.Audio )			{				Debug.LogError( "Picking audio files is not supported on iOS" );				if( callback != null ) // Selecting audio files is not supported on iOS					callback( null );			}			else			{				NGMediaReceiveCallbackiOS.Initialize( callback, null );				_NativeGallery_PickMedia( SelectedMediaPath, (int) ( mediaType & ~MediaType.Audio ), PermissionFreeMode ? 1 : 0, 1 );			}#else			if( callback != null )				callback( null );#endif		}		return result;	}	private static Permission GetMultipleMediaFromGallery( MediaPickMultipleCallback callback, MediaType mediaType, string mime, string title )	{		Permission result = RequestPermission( PermissionType.Read, mediaType );		if( result == Permission.Granted && !IsMediaPickerBusy() )		{			if( CanSelectMultipleFilesFromGallery() )			{#if !UNITY_EDITOR && UNITY_ANDROID				AJC.CallStatic( "PickMedia", Context, new NGMediaReceiveCallbackAndroid( null, callback ), (int) mediaType, true, SelectedMediaPath, mime, title );#elif !UNITY_EDITOR && UNITY_IOS				if( mediaType == MediaType.Audio )				{					Debug.LogError( "Picking audio files is not supported on iOS" );					if( callback != null ) // Selecting audio files is not supported on iOS						callback( null );				}				else				{					NGMediaReceiveCallbackiOS.Initialize( null, callback );					_NativeGallery_PickMedia( SelectedMediaPath, (int) ( mediaType & ~MediaType.Audio ), PermissionFreeMode ? 1 : 0, 0 );				}#else				if( callback != null )					callback( null );#endif			}			else if( callback != null )				callback( null );		}		return result;	}	private static byte[] GetTextureBytes( Texture2D texture, bool isJpeg )	{		try		{			return isJpeg ? texture.EncodeToJPG( 100 ) : texture.EncodeToPNG();		}		catch( UnityException )		{			return GetTextureBytesFromCopy( texture, isJpeg );		}		catch( ArgumentException )		{			return GetTextureBytesFromCopy( texture, isJpeg );		}#pragma warning disable 0162		return null;#pragma warning restore 0162	}	private static byte[] GetTextureBytesFromCopy( Texture2D texture, bool isJpeg )	{		// Texture is marked as non-readable, create a readable copy and save it instead		Debug.LogWarning( "Saving non-readable textures is slower than saving readable textures" );		Texture2D sourceTexReadable = null;		RenderTexture rt = RenderTexture.GetTemporary( texture.width, texture.height );		RenderTexture activeRT = RenderTexture.active;		try		{			Graphics.Blit( texture, rt );			RenderTexture.active = rt;			sourceTexReadable = new Texture2D( texture.width, texture.height, isJpeg ? TextureFormat.RGB24 : TextureFormat.RGBA32, false );			sourceTexReadable.ReadPixels( new Rect( 0, 0, texture.width, texture.height ), 0, 0, false );			sourceTexReadable.Apply( false, false );		}		catch( Exception e )		{			Debug.LogException( e );			Object.DestroyImmediate( sourceTexReadable );			return null;		}		finally		{			RenderTexture.active = activeRT;			RenderTexture.ReleaseTemporary( rt );		}		try		{			return isJpeg ? sourceTexReadable.EncodeToJPG( 100 ) : sourceTexReadable.EncodeToPNG();		}		catch( Exception e )		{			Debug.LogException( e );			return null;		}		finally		{			Object.DestroyImmediate( sourceTexReadable );		}	}#if UNITY_2018_4_OR_NEWER && !NATIVE_GALLERY_DISABLE_ASYNC_FUNCTIONS	private static async Task<T> TryCallNativeAndroidFunctionOnSeparateThread<T>( Func<T> function )	{		T result = default( T );		bool hasResult = false;		await Task.Run( () =>		{			if( AndroidJNI.AttachCurrentThread() != 0 )				Debug.LogWarning( "Couldn't attach JNI thread, calling native function on the main thread" );			else			{				try				{					result = function();					hasResult = true;				}				finally				{					AndroidJNI.DetachCurrentThread();				}			}		} );		return hasResult ? result : function();	}#endif	#endregion	#region Utility Functions	public static Texture2D LoadImageAtPath( string imagePath, int maxSize = -1, bool markTextureNonReadable = true, bool generateMipmaps = true, bool linearColorSpace = false )	{		if( string.IsNullOrEmpty( imagePath ) )			throw new ArgumentException( "Parameter 'imagePath' is null or empty!" );		if( !File.Exists( imagePath ) )			throw new FileNotFoundException( "File not found at " + imagePath );		if( maxSize <= 0 )			maxSize = SystemInfo.maxTextureSize;#if !UNITY_EDITOR && UNITY_ANDROID		string loadPath = AJC.CallStatic<string>( "LoadImageAtPath", Context, imagePath, TemporaryImagePath, maxSize );#elif !UNITY_EDITOR && UNITY_IOS		string loadPath = _NativeGallery_LoadImageAtPath( imagePath, TemporaryImagePath, maxSize );#else		string loadPath = imagePath;#endif		string extension = Path.GetExtension( imagePath ).ToLowerInvariant();		TextureFormat format = ( extension == ".jpg" || extension == ".jpeg" ) ? TextureFormat.RGB24 : TextureFormat.RGBA32;		Texture2D result = new Texture2D( 2, 2, format, generateMipmaps, linearColorSpace );		try		{			if( !result.LoadImage( File.ReadAllBytes( loadPath ), markTextureNonReadable ) )			{				Debug.LogWarning( "Couldn't load image at path: " + loadPath );				Object.DestroyImmediate( result );				return null;			}		}		catch( Exception e )		{			Debug.LogException( e );			Object.DestroyImmediate( result );			return null;		}		finally		{			if( loadPath != imagePath )			{				try				{					File.Delete( loadPath );				}				catch { }			}		}		return result;	}#if UNITY_2018_4_OR_NEWER && !NATIVE_GALLERY_DISABLE_ASYNC_FUNCTIONS	public static async Task<Texture2D> LoadImageAtPathAsync( string imagePath, int maxSize = -1, bool markTextureNonReadable = true )	{		if( string.IsNullOrEmpty( imagePath ) )			throw new ArgumentException( "Parameter 'imagePath' is null or empty!" );		if( !File.Exists( imagePath ) )			throw new FileNotFoundException( "File not found at " + imagePath );		if( maxSize <= 0 )			maxSize = SystemInfo.maxTextureSize;#if !UNITY_EDITOR && UNITY_ANDROID		string temporaryImagePath = TemporaryImagePath; // Must be accessed from main thread		string loadPath = await TryCallNativeAndroidFunctionOnSeparateThread( () => AJC.CallStatic<string>( "LoadImageAtPath", Context, imagePath, temporaryImagePath, maxSize ) );#elif !UNITY_EDITOR && UNITY_IOS		string temporaryImagePath = TemporaryImagePath; // Must be accessed from main thread		string loadPath = await Task.Run( () => _NativeGallery_LoadImageAtPath( imagePath, temporaryImagePath, maxSize ) );#else		string loadPath = imagePath;#endif		Texture2D result = null;		using( UnityWebRequest www = UnityWebRequestTexture.GetTexture( "file://" + loadPath, markTextureNonReadable ) )		{			UnityWebRequestAsyncOperation asyncOperation = www.SendWebRequest();			while( !asyncOperation.isDone )				await Task.Yield();#if UNITY_2020_1_OR_NEWER			if( www.result != UnityWebRequest.Result.Success )#else			if( www.isNetworkError || www.isHttpError )#endif			{				Debug.LogWarning( "Couldn't use UnityWebRequest to load image, falling back to LoadImage: " + www.error );			}			else				result = DownloadHandlerTexture.GetContent( www );		}		if( !result ) // Fallback to Texture2D.LoadImage if something goes wrong		{			string extension = Path.GetExtension( imagePath ).ToLowerInvariant();			TextureFormat format = ( extension == ".jpg" || extension == ".jpeg" ) ? TextureFormat.RGB24 : TextureFormat.RGBA32;			result = new Texture2D( 2, 2, format, true, false );			try			{				if( !result.LoadImage( File.ReadAllBytes( loadPath ), markTextureNonReadable ) )				{					Debug.LogWarning( "Couldn't load image at path: " + loadPath );					Object.DestroyImmediate( result );					return null;				}			}			catch( Exception e )			{				Debug.LogException( e );				Object.DestroyImmediate( result );				return null;			}			finally			{				if( loadPath != imagePath )				{					try					{						File.Delete( loadPath );					}					catch { }				}			}		}		return result;	}#endif	public static Texture2D GetVideoThumbnail( string videoPath, int maxSize = -1, double captureTimeInSeconds = -1.0, bool markTextureNonReadable = true, bool generateMipmaps = true, bool linearColorSpace = false )	{		if( maxSize <= 0 )			maxSize = SystemInfo.maxTextureSize;#if !UNITY_EDITOR && UNITY_ANDROID		string thumbnailPath = AJC.CallStatic<string>( "GetVideoThumbnail", Context, videoPath, TemporaryImagePath + ".png", false, maxSize, captureTimeInSeconds );#elif !UNITY_EDITOR && UNITY_IOS		string thumbnailPath = _NativeGallery_GetVideoThumbnail( videoPath, TemporaryImagePath + ".png", maxSize, captureTimeInSeconds );#else		string thumbnailPath = null;#endif		if( !string.IsNullOrEmpty( thumbnailPath ) )			return LoadImageAtPath( thumbnailPath, maxSize, markTextureNonReadable, generateMipmaps, linearColorSpace );		else			return null;	}#if UNITY_2018_4_OR_NEWER && !NATIVE_GALLERY_DISABLE_ASYNC_FUNCTIONS	public static async Task<Texture2D> GetVideoThumbnailAsync( string videoPath, int maxSize = -1, double captureTimeInSeconds = -1.0, bool markTextureNonReadable = true )	{		if( maxSize <= 0 )			maxSize = SystemInfo.maxTextureSize;#if !UNITY_EDITOR && UNITY_ANDROID		string temporaryImagePath = TemporaryImagePath; // Must be accessed from main thread		string thumbnailPath = await TryCallNativeAndroidFunctionOnSeparateThread( () => AJC.CallStatic<string>( "GetVideoThumbnail", Context, videoPath, temporaryImagePath + ".png", false, maxSize, captureTimeInSeconds ) );#elif !UNITY_EDITOR && UNITY_IOS		string temporaryImagePath = TemporaryImagePath; // Must be accessed from main thread		string thumbnailPath = await Task.Run( () => _NativeGallery_GetVideoThumbnail( videoPath, temporaryImagePath + ".png", maxSize, captureTimeInSeconds ) );#else		string thumbnailPath = null;#endif		if( !string.IsNullOrEmpty( thumbnailPath ) )			return await LoadImageAtPathAsync( thumbnailPath, maxSize, markTextureNonReadable );		else			return null;	}#endif	public static ImageProperties GetImageProperties( string imagePath )	{		if( !File.Exists( imagePath ) )			throw new FileNotFoundException( "File not found at " + imagePath );#if !UNITY_EDITOR && UNITY_ANDROID		string value = AJC.CallStatic<string>( "GetImageProperties", Context, imagePath );#elif !UNITY_EDITOR && UNITY_IOS		string value = _NativeGallery_GetImageProperties( imagePath );#else		string value = null;#endif		int width = 0, height = 0;		string mimeType = null;		ImageOrientation orientation = ImageOrientation.Unknown;		if( !string.IsNullOrEmpty( value ) )		{			string[] properties = value.Split( '>' );			if( properties != null && properties.Length >= 4 )			{				if( !int.TryParse( properties[0].Trim(), out width ) )					width = 0;				if( !int.TryParse( properties[1].Trim(), out height ) )					height = 0;				mimeType = properties[2].Trim();				if( mimeType.Length == 0 )				{					string extension = Path.GetExtension( imagePath ).ToLowerInvariant();					if( extension == ".png" )						mimeType = "image/png";					else if( extension == ".jpg" || extension == ".jpeg" )						mimeType = "image/jpeg";					else if( extension == ".gif" )						mimeType = "image/gif";					else if( extension == ".bmp" )						mimeType = "image/bmp";					else						mimeType = null;				}				int orientationInt;				if( int.TryParse( properties[3].Trim(), out orientationInt ) )					orientation = (ImageOrientation) orientationInt;			}		}		return new ImageProperties( width, height, mimeType, orientation );	}	public static VideoProperties GetVideoProperties( string videoPath )	{		if( !File.Exists( videoPath ) )			throw new FileNotFoundException( "File not found at " + videoPath );#if !UNITY_EDITOR && UNITY_ANDROID		string value = AJC.CallStatic<string>( "GetVideoProperties", Context, videoPath );#elif !UNITY_EDITOR && UNITY_IOS		string value = _NativeGallery_GetVideoProperties( videoPath );#else		string value = null;#endif		int width = 0, height = 0;		long duration = 0L;		float rotation = 0f;		if( !string.IsNullOrEmpty( value ) )		{			string[] properties = value.Split( '>' );			if( properties != null && properties.Length >= 4 )			{				if( !int.TryParse( properties[0].Trim(), out width ) )					width = 0;				if( !int.TryParse( properties[1].Trim(), out height ) )					height = 0;				if( !long.TryParse( properties[2].Trim(), out duration ) )					duration = 0L;				if( !float.TryParse( properties[3].Trim().Replace( ',', '.' ), NumberStyles.Float, CultureInfo.InvariantCulture, out rotation ) )					rotation = 0f;			}		}		if( rotation == -90f )			rotation = 270f;		return new VideoProperties( width, height, duration, rotation );	}	#endregion}
 |