| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589 | #import <Foundation/Foundation.h>#import <Photos/Photos.h>#import <MobileCoreServices/UTCoreTypes.h>#import <MobileCoreServices/MobileCoreServices.h>#import <ImageIO/ImageIO.h>#if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000#import <AssetsLibrary/AssetsLibrary.h>#endif#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000#import <PhotosUI/PhotosUI.h>#endif#ifdef UNITY_4_0 || UNITY_5_0#import "iPhone_View.h"#elseextern UIViewController* UnityGetGLViewController();#endif#define CHECK_IOS_VERSION( version )  ([[[UIDevice currentDevice] systemVersion] compare:version options:NSNumericSearch] != NSOrderedAscending)@interface UNativeGallery:NSObject+ (int)checkPermission:(BOOL)readPermission permissionFreeMode:(BOOL)permissionFreeMode;+ (int)requestPermission:(BOOL)readPermission permissionFreeMode:(BOOL)permissionFreeMode asyncMode:(BOOL)asyncMode;+ (void)showLimitedLibraryPicker;+ (int)canOpenSettings;+ (void)openSettings;+ (int)canPickMultipleMedia;+ (void)saveMedia:(NSString *)path albumName:(NSString *)album isImg:(BOOL)isImg permissionFreeMode:(BOOL)permissionFreeMode;+ (void)pickMedia:(int)mediaType savePath:(NSString *)mediaSavePath permissionFreeMode:(BOOL)permissionFreeMode selectionLimit:(int)selectionLimit;+ (int)isMediaPickerBusy;+ (int)getMediaTypeFromExtension:(NSString *)extension;+ (char *)getImageProperties:(NSString *)path;+ (char *)getVideoProperties:(NSString *)path;+ (char *)getVideoThumbnail:(NSString *)path savePath:(NSString *)savePath maximumSize:(int)maximumSize captureTime:(double)captureTime;+ (char *)loadImageAtPath:(NSString *)path tempFilePath:(NSString *)tempFilePath maximumSize:(int)maximumSize;@end@implementation UNativeGallerystatic NSString *pickedMediaSavePath;static UIPopoverController *popup;static UIImagePickerController *imagePicker;#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000static PHPickerViewController *imagePickerNew;#endifstatic int imagePickerState = 0; // 0 -> none, 1 -> showing (always in this state on iPad), 2 -> finishedstatic BOOL simpleMediaPickMode;static BOOL pickingMultipleFiles = NO;#pragma clang diagnostic push#pragma clang diagnostic ignored "-Wdeprecated-declarations"+ (int)checkPermission:(BOOL)readPermission permissionFreeMode:(BOOL)permissionFreeMode{#if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000	if( CHECK_IOS_VERSION( @"8.0" ) )	{#endif		// version >= iOS 8: check permission using Photos framework		// On iOS 11 and later, permission isn't mandatory to fetch media from Photos		if( readPermission && permissionFreeMode && CHECK_IOS_VERSION( @"11.0" ) )			return 1;		#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000		// Photos permissions has changed on iOS 14		if( CHECK_IOS_VERSION( @"14.0" ) )		{			// Request ReadWrite permission in 2 cases:			// 1) When attempting to pick media from Photos with PHPhotoLibrary (readPermission=true and permissionFreeMode=false)			// 2) When attempting to write media to a specific album in Photos using PHPhotoLibrary (readPermission=false and permissionFreeMode=false)			PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatusForAccessLevel:( ( readPermission || !permissionFreeMode ) ? PHAccessLevelReadWrite : PHAccessLevelAddOnly )];			if( status == PHAuthorizationStatusAuthorized )				return 1;			else if( status == PHAuthorizationStatusRestricted )				return 3;			else if( status == PHAuthorizationStatusNotDetermined )				return 2;			else				return 0;		}		else#endif		{			PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus];			if( status == PHAuthorizationStatusAuthorized )				return 1;			else if( status == PHAuthorizationStatusNotDetermined )				return 2;			else				return 0;		}#if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000	}	else	{		// version < iOS 8: check permission using AssetsLibrary framework (Photos framework not available)		ALAuthorizationStatus status = [ALAssetsLibrary authorizationStatus];		if( status == ALAuthorizationStatusAuthorized )			return 1;		else if( status == ALAuthorizationStatusNotDetermined )			return 2;		else			return 0;	}#endif}#pragma clang diagnostic pop+ (int)requestPermission:(BOOL)readPermission permissionFreeMode:(BOOL)permissionFreeMode asyncMode:(BOOL)asyncMode{	int result;#if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000	if( CHECK_IOS_VERSION( @"8.0" ) )	{#endif		// version >= iOS 8: request permission using Photos framework				// On iOS 11 and later, permission isn't mandatory to fetch media from Photos		if( readPermission && permissionFreeMode && CHECK_IOS_VERSION( @"11.0" ) )			result = 1;#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000		else if( CHECK_IOS_VERSION( @"14.0" ) )		{			// Photos permissions has changed on iOS 14. There are 2 permission dialogs now:			// - AddOnly permission dialog: has 2 options: "Allow" and "Don't Allow". This dialog grants permission for save operations only. Unfortunately,			//   saving media to a custom album isn't possible with this dialog, media can only be saved to the default Photos album			// - ReadWrite permission dialog: has 3 options: "Allow Access to All Photos" (i.e. full permission), "Select Photos" (i.e. limited access) and			//   "Don't Allow". To be able to save media to a custom album, user must grant Full Photos permission. Thus, even when readPermission is false,			//   this dialog will be used if PermissionFreeMode is set to false. So, PermissionFreeMode determines whether or not saving to a custom album is			//   be supported			result = [self requestPermissionNewest:( readPermission || !permissionFreeMode ) asyncMode:asyncMode];		}#endif		else			result = [self requestPermissionNew:asyncMode];#if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000	}	else	{		// version < iOS 8: request permission using AssetsLibrary framework (Photos framework not available)		result = [self requestPermissionOld:asyncMode];	}		if( asyncMode && result >= 0 ) // Result returned immediately, forward it		UnitySendMessage( "NGPermissionCallbackiOS", "OnPermissionRequested", [self getCString:[NSString stringWithFormat:@"%d", result]] );			return result;#endif}#if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000// Credit: https://stackoverflow.com/a/26933380/2373034#pragma clang diagnostic push#pragma clang diagnostic ignored "-Wdeprecated-declarations"+ (int)requestPermissionOld:(BOOL)asyncMode{	ALAuthorizationStatus status = [ALAssetsLibrary authorizationStatus];		if( status == ALAuthorizationStatusAuthorized )		return 1;	else if( status == ALAuthorizationStatusNotDetermined )	{		if( asyncMode )		{			[[[ALAssetsLibrary alloc] init] enumerateGroupsWithTypes:ALAssetsGroupAll usingBlock:^( ALAssetsGroup *group, BOOL *stop )			{				*stop = YES;				UnitySendMessage( "NGPermissionCallbackiOS", "OnPermissionRequested", "1" );			}			failureBlock:^( NSError *error )			{				UnitySendMessage( "NGPermissionCallbackiOS", "OnPermissionRequested", "0" );			}];						return -1;		}		else		{			__block BOOL authorized = NO;			dispatch_semaphore_t sema = dispatch_semaphore_create( 0 );			[[[ALAssetsLibrary alloc] init] enumerateGroupsWithTypes:ALAssetsGroupAll usingBlock:^( ALAssetsGroup *group, BOOL *stop )			{				*stop = YES;				authorized = YES;				dispatch_semaphore_signal( sema );			}			failureBlock:^( NSError *error )			{				dispatch_semaphore_signal( sema );			}];			dispatch_semaphore_wait( sema, DISPATCH_TIME_FOREVER );						return authorized ? 1 : 0;		}	}	return 0;}#pragma clang diagnostic pop#endif// Credit: https://stackoverflow.com/a/32989022/2373034+ (int)requestPermissionNew:(BOOL)asyncMode{	PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus];		if( status == PHAuthorizationStatusAuthorized )		return 1;	else if( status == PHAuthorizationStatusNotDetermined )	{		if( asyncMode )		{			[PHPhotoLibrary requestAuthorization:^( PHAuthorizationStatus status )			{				UnitySendMessage( "NGPermissionCallbackiOS", "OnPermissionRequested", ( status == PHAuthorizationStatusAuthorized ) ? "1" : "0" );			}];						return -1;		}		else		{			__block BOOL authorized = NO;						dispatch_semaphore_t sema = dispatch_semaphore_create( 0 );			[PHPhotoLibrary requestAuthorization:^( PHAuthorizationStatus status )			{				authorized = ( status == PHAuthorizationStatusAuthorized );				dispatch_semaphore_signal( sema );			}];			dispatch_semaphore_wait( sema, DISPATCH_TIME_FOREVER );						return authorized ? 1 : 0;		}	}		return 0;}#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000+ (int)requestPermissionNewest:(BOOL)readPermission asyncMode:(BOOL)asyncMode{	PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatusForAccessLevel:( readPermission ? PHAccessLevelReadWrite : PHAccessLevelAddOnly )];		if( status == PHAuthorizationStatusAuthorized )		return 1;	else if( status == PHAuthorizationStatusRestricted )		return 3;	else if( status == PHAuthorizationStatusNotDetermined )	{		if( asyncMode )		{			[PHPhotoLibrary requestAuthorizationForAccessLevel:( readPermission ? PHAccessLevelReadWrite : PHAccessLevelAddOnly ) handler:^( PHAuthorizationStatus status )			{				if( status == PHAuthorizationStatusAuthorized )					UnitySendMessage( "NGPermissionCallbackiOS", "OnPermissionRequested", "1" );				else if( status == PHAuthorizationStatusRestricted )					UnitySendMessage( "NGPermissionCallbackiOS", "OnPermissionRequested", "3" );				else					UnitySendMessage( "NGPermissionCallbackiOS", "OnPermissionRequested", "0" );			}];						return -1;		}		else		{			__block int authorized = 0;						dispatch_semaphore_t sema = dispatch_semaphore_create( 0 );			[PHPhotoLibrary requestAuthorizationForAccessLevel:( readPermission ? PHAccessLevelReadWrite : PHAccessLevelAddOnly ) handler:^( PHAuthorizationStatus status )			{				if( status == PHAuthorizationStatusAuthorized )					authorized = 1;				else if( status == PHAuthorizationStatusRestricted )					authorized = 3;				dispatch_semaphore_signal( sema );			}];			dispatch_semaphore_wait( sema, DISPATCH_TIME_FOREVER );						return authorized;		}	}		return 0;}#endif// When Photos permission is set to restricted, allows user to change the permission or change the list of restricted images// It doesn't support a deterministic callback; for example there is a photoLibraryDidChange event but it won't be invoked if// user doesn't change the list of restricted images+ (void)showLimitedLibraryPicker{#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000	PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatusForAccessLevel:PHAccessLevelReadWrite];	if( status == PHAuthorizationStatusNotDetermined )		[self requestPermissionNewest:YES asyncMode:YES];	else if( status == PHAuthorizationStatusRestricted )		[[PHPhotoLibrary sharedPhotoLibrary] presentLimitedLibraryPickerFromViewController:UnityGetGLViewController()];#endif}// Credit: https://stackoverflow.com/a/25453667/2373034+ (int)canOpenSettings{	return ( &UIApplicationOpenSettingsURLString != NULL ) ? 1 : 0;}// Credit: https://stackoverflow.com/a/25453667/2373034#pragma clang diagnostic push#pragma clang diagnostic ignored "-Wdeprecated-declarations"+ (void)openSettings{	if( &UIApplicationOpenSettingsURLString != NULL )	{#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 100000		if( CHECK_IOS_VERSION( @"10.0" ) )			[[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString] options:@{} completionHandler:nil];		else#endif			[[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]];	}}#pragma clang diagnostic pop+ (int)canPickMultipleMedia{#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000	if( CHECK_IOS_VERSION( @"14.0" ) )		return 1;	else#endif		return 0;}+ (void)saveMedia:(NSString *)path albumName:(NSString *)album isImg:(BOOL)isImg permissionFreeMode:(BOOL)permissionFreeMode{#if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000	if( CHECK_IOS_VERSION( @"8.0" ) )	{#endif		// version >= iOS 8: save to specified album using Photos framework		// On iOS 14+, permission workflow has changed significantly with the addition of PHAuthorizationStatusRestricted permission. On those versions,		// user must grant Full Photos permission to be able to save to a custom album. Hence, there are 2 workflows:		// - If PermissionFreeMode is enabled, save the media directly to the default album (i.e. ignore 'album' parameter). This will present a simple		//   permission dialog stating "The app requires access to Photos to save media to it." and the "Selected Photos" permission won't be listed in the options		// - Otherwise, the more complex "The app requires access to Photos to interact with it." permission dialog will be shown and if the user grants		//   Full Photos permission, only then the image will be saved to the specified album. If user selects "Selected Photos" permission, default album will be		//   used as fallback		[self saveMediaNew:path albumName:album isImage:isImg saveToDefaultAlbum:( permissionFreeMode && CHECK_IOS_VERSION( @"14.0" ) )];#if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000	}	else	{		// version < iOS 8: save using AssetsLibrary framework (Photos framework not available)		[self saveMediaOld:path albumName:album isImage:isImg];	}#endif}#if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000// Credit: https://stackoverflow.com/a/22056664/2373034#pragma clang diagnostic push#pragma clang diagnostic ignored "-Wdeprecated-declarations"+ (void)saveMediaOld:(NSString *)path albumName:(NSString *)album isImage:(BOOL)isImage{	ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];		if( !isImage && ![library videoAtPathIsCompatibleWithSavedPhotosAlbum:[NSURL fileURLWithPath:path]])	{		NSLog( @"Error saving video: Video format is not compatible with Photos" );		[[NSFileManager defaultManager] removeItemAtPath:path error:nil];		UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" );		return;	}		void (^saveBlock)(ALAssetsGroup *assetCollection) = ^void( ALAssetsGroup *assetCollection )	{		void (^saveResultBlock)(NSURL *assetURL, NSError *error) = ^void( NSURL *assetURL, NSError *error )		{			[[NSFileManager defaultManager] removeItemAtPath:path error:nil];						if( error.code == 0 )			{				[library assetForURL:assetURL resultBlock:^( ALAsset *asset )				{					[assetCollection addAsset:asset];					UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveCompleted", "" );				}				failureBlock:^( NSError* error )				{					NSLog( @"Error moving asset to album: %@", error );					UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" );				}];			}			else			{				NSLog( @"Error creating asset: %@", error );				UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" );			}		};				if( !isImage )			[library writeImageDataToSavedPhotosAlbum:[NSData dataWithContentsOfFile:path] metadata:nil completionBlock:saveResultBlock];		else			[library writeVideoAtPathToSavedPhotosAlbum:[NSURL fileURLWithPath:path] completionBlock:saveResultBlock];	};		__block BOOL albumFound = NO;	[library enumerateGroupsWithTypes:ALAssetsGroupAlbum usingBlock:^( ALAssetsGroup *group, BOOL *stop )	{		if( [[group valueForProperty:ALAssetsGroupPropertyName] isEqualToString:album] )		{			*stop = YES;			albumFound = YES;			saveBlock( group );		}		else if( group == nil && albumFound==NO )		{			// Album doesn't exist			[library addAssetsGroupAlbumWithName:album resultBlock:^( ALAssetsGroup *group )			{				saveBlock( group );			}			failureBlock:^( NSError *error )			{				NSLog( @"Error creating album: %@", error );				[[NSFileManager defaultManager] removeItemAtPath:path error:nil];				UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" );			}];		}	}	failureBlock:^( NSError* error )	{		NSLog( @"Error listing albums: %@", error );		[[NSFileManager defaultManager] removeItemAtPath:path error:nil];		UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" );	}];}#pragma clang diagnostic pop#endif// Credit: https://stackoverflow.com/a/39909129/2373034+ (void)saveMediaNew:(NSString *)path albumName:(NSString *)album isImage:(BOOL)isImage saveToDefaultAlbum:(BOOL)saveToDefaultAlbum{	void (^saveToPhotosAlbum)() = ^void()	{		if( isImage )		{			// Try preserving image metadata (essential for animated gif images)			[[PHPhotoLibrary sharedPhotoLibrary] performChanges:			^{				[PHAssetChangeRequest creationRequestForAssetFromImageAtFileURL:[NSURL fileURLWithPath:path]];			}			completionHandler:^( BOOL success, NSError *error )			{				if( success )				{					[[NSFileManager defaultManager] removeItemAtPath:path error:nil];					UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveCompleted", "" );				}				else				{					NSLog( @"Error creating asset in default Photos album: %@", error );										UIImage *image = [UIImage imageWithContentsOfFile:path];					if( image != nil )						UIImageWriteToSavedPhotosAlbum( image, self, @selector(image:didFinishSavingWithError:contextInfo:), (__bridge_retained void *) path );					else					{						NSLog( @"Couldn't create UIImage from file at path: %@", path );						[[NSFileManager defaultManager] removeItemAtPath:path error:nil];						UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" );					}				}			}];		}		else		{			if( UIVideoAtPathIsCompatibleWithSavedPhotosAlbum( path ) )				UISaveVideoAtPathToSavedPhotosAlbum( path, self, @selector(video:didFinishSavingWithError:contextInfo:), (__bridge_retained void *) path );			else			{				NSLog( @"Video at path isn't compatible with saved photos album: %@", path );				[[NSFileManager defaultManager] removeItemAtPath:path error:nil];				UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" );			}		}	};	void (^saveBlock)(PHAssetCollection *assetCollection) = ^void( PHAssetCollection *assetCollection )	{		[[PHPhotoLibrary sharedPhotoLibrary] performChanges:		^{			PHAssetChangeRequest *assetChangeRequest;			if( isImage )				assetChangeRequest = [PHAssetChangeRequest creationRequestForAssetFromImageAtFileURL:[NSURL fileURLWithPath:path]];			else				assetChangeRequest = [PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:[NSURL fileURLWithPath:path]];						PHAssetCollectionChangeRequest *assetCollectionChangeRequest = [PHAssetCollectionChangeRequest changeRequestForAssetCollection:assetCollection];			[assetCollectionChangeRequest addAssets:@[[assetChangeRequest placeholderForCreatedAsset]]];					}		completionHandler:^( BOOL success, NSError *error )		{			if( success )			{				[[NSFileManager defaultManager] removeItemAtPath:path error:nil];				UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveCompleted", "" );			}			else			{				NSLog( @"Error creating asset: %@", error );				saveToPhotosAlbum();			}		}];	};	if( saveToDefaultAlbum )		saveToPhotosAlbum();	else	{		PHFetchOptions *fetchOptions = [[PHFetchOptions alloc] init];		fetchOptions.predicate = [NSPredicate predicateWithFormat:@"localizedTitle = %@", album];		PHFetchResult *fetchResult = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeAlbum subtype:PHAssetCollectionSubtypeAny options:fetchOptions];		if( fetchResult.count > 0 )			saveBlock( fetchResult.firstObject);		else		{			__block PHObjectPlaceholder *albumPlaceholder;			[[PHPhotoLibrary sharedPhotoLibrary] performChanges:			^{				PHAssetCollectionChangeRequest *changeRequest = [PHAssetCollectionChangeRequest creationRequestForAssetCollectionWithTitle:album];				albumPlaceholder = changeRequest.placeholderForCreatedAssetCollection;			}			completionHandler:^( BOOL success, NSError *error )			{				if( success )				{					PHFetchResult *fetchResult = [PHAssetCollection fetchAssetCollectionsWithLocalIdentifiers:@[albumPlaceholder.localIdentifier] options:nil];					if( fetchResult.count > 0 )						saveBlock( fetchResult.firstObject);					else					{						NSLog( @"Error creating album: Album placeholder not found" );						saveToPhotosAlbum();					}				}				else				{					NSLog( @"Error creating album: %@", error );					saveToPhotosAlbum();				}			}];		}	}}+ (void)image:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo{	NSString* path = (__bridge_transfer NSString *)(contextInfo);	[[NSFileManager defaultManager] removeItemAtPath:path error:nil];	if( error == nil )		UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveCompleted", "" );	else	{		NSLog( @"Error saving image with UIImageWriteToSavedPhotosAlbum: %@", error );		UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" );	}}+ (void)video:(NSString *)videoPath didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo{	NSString* path = (__bridge_transfer NSString *)(contextInfo);	[[NSFileManager defaultManager] removeItemAtPath:path error:nil];	if( error == nil )		UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveCompleted", "" );	else	{		NSLog( @"Error saving video with UISaveVideoAtPathToSavedPhotosAlbum: %@", error );		UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" );	}}// Credit: https://stackoverflow.com/a/10531752/2373034+ (void)pickMedia:(int)mediaType savePath:(NSString *)mediaSavePath permissionFreeMode:(BOOL)permissionFreeMode selectionLimit:(int)selectionLimit{	pickedMediaSavePath = mediaSavePath;	imagePickerState = 1;	simpleMediaPickMode = permissionFreeMode && CHECK_IOS_VERSION( @"11.0" );	#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000	if( CHECK_IOS_VERSION( @"14.0" ) )	{		// PHPickerViewController is used on iOS 14		PHPickerConfiguration *config = simpleMediaPickMode ? [[PHPickerConfiguration alloc] init] : [[PHPickerConfiguration alloc] initWithPhotoLibrary:[PHPhotoLibrary sharedPhotoLibrary]];		config.preferredAssetRepresentationMode = PHPickerConfigurationAssetRepresentationModeCurrent;		config.selectionLimit = selectionLimit;		pickingMultipleFiles = selectionLimit != 1;				// mediaType is a bitmask:		// 1: image		// 2: video		// 4: audio (not supported)		if( mediaType == 1 )			config.filter = [PHPickerFilter anyFilterMatchingSubfilters:[NSArray arrayWithObjects:[PHPickerFilter imagesFilter], [PHPickerFilter livePhotosFilter], nil]];		else if( mediaType == 2 )			config.filter = [PHPickerFilter videosFilter];		else			config.filter = [PHPickerFilter anyFilterMatchingSubfilters:[NSArray arrayWithObjects:[PHPickerFilter imagesFilter], [PHPickerFilter livePhotosFilter], [PHPickerFilter videosFilter], nil]];				imagePickerNew = [[PHPickerViewController alloc] initWithConfiguration:config];		imagePickerNew.delegate = (id) self;		[UnityGetGLViewController() presentViewController:imagePickerNew animated:YES completion:^{ imagePickerState = 0; }];	}	else#endif	{		// UIImagePickerController is used on previous versions		imagePicker = [[UIImagePickerController alloc] init];		imagePicker.delegate = (id) self;		imagePicker.allowsEditing = NO;		imagePicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;				// mediaType is a bitmask:		// 1: image		// 2: video		// 4: audio (not supported)		if( mediaType == 1 )		{			if( CHECK_IOS_VERSION( @"9.1" ) )				imagePicker.mediaTypes = [NSArray arrayWithObjects:(NSString *)kUTTypeImage, (NSString *)kUTTypeLivePhoto, nil];			else				imagePicker.mediaTypes = [NSArray arrayWithObject:(NSString *)kUTTypeImage];		}		else if( mediaType == 2 )			imagePicker.mediaTypes = [NSArray arrayWithObjects:(NSString *)kUTTypeMovie, (NSString *)kUTTypeVideo, nil];		else		{			if( CHECK_IOS_VERSION( @"9.1" ) )				imagePicker.mediaTypes = [NSArray arrayWithObjects:(NSString *)kUTTypeImage, (NSString *)kUTTypeLivePhoto, (NSString *)kUTTypeMovie, (NSString *)kUTTypeVideo, nil];			else				imagePicker.mediaTypes = [NSArray arrayWithObjects:(NSString *)kUTTypeImage, (NSString *)kUTTypeMovie, (NSString *)kUTTypeVideo, nil];		}		#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000		if( mediaType != 1 )		{			// Don't compress picked videos if possible			if( CHECK_IOS_VERSION( @"11.0" ) )				imagePicker.videoExportPreset = AVAssetExportPresetPassthrough;		}#endif				UIViewController *rootViewController = UnityGetGLViewController();		if( UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone ) // iPhone			[rootViewController presentViewController:imagePicker animated:YES completion:^{ imagePickerState = 0; }];		else		{			// iPad			popup = [[UIPopoverController alloc] initWithContentViewController:imagePicker];			popup.delegate = (id) self;			[popup presentPopoverFromRect:CGRectMake( rootViewController.view.frame.size.width / 2, rootViewController.view.frame.size.height / 2, 1, 1 ) inView:rootViewController.view permittedArrowDirections:0 animated:YES];		}	}}+ (int)isMediaPickerBusy{	if( imagePickerState == 2 )		return 1;		if( imagePicker != nil )	{		if( imagePickerState == 1 || [imagePicker presentingViewController] == UnityGetGLViewController() )			return 1;		else		{			imagePicker = nil;			return 0;		}	}#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000	else if( CHECK_IOS_VERSION( @"14.0" ) && imagePickerNew != nil )	{		if( imagePickerState == 1 || [imagePickerNew presentingViewController] == UnityGetGLViewController() )			return 1;		else		{			imagePickerNew = nil;			return 0;		}	}#endif	else		return 0;}#pragma clang diagnostic push#pragma clang diagnostic ignored "-Wdeprecated-declarations"+ (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info{	NSString *resultPath = nil;		if( [info[UIImagePickerControllerMediaType] isEqualToString:(NSString *)kUTTypeImage] )	{		NSLog( @"Picked an image" );				// On iOS 8.0 or later, try to obtain the raw data of the image (which allows picking gifs properly or preserving metadata)		if( CHECK_IOS_VERSION( @"8.0" ) )		{			PHAsset *asset = nil;#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000			if( CHECK_IOS_VERSION( @"11.0" ) )			{				// Try fetching the source image via UIImagePickerControllerImageURL				NSURL *mediaUrl = info[UIImagePickerControllerImageURL];				if( mediaUrl != nil )				{					NSString *imagePath = [mediaUrl path];					if( imagePath != nil && [[NSFileManager defaultManager] fileExistsAtPath:imagePath] )					{						NSError *error;						NSString *newPath = [pickedMediaSavePath stringByAppendingPathExtension:[imagePath pathExtension]];												if( ![[NSFileManager defaultManager] fileExistsAtPath:newPath] || [[NSFileManager defaultManager] removeItemAtPath:newPath error:&error] )						{							if( [[NSFileManager defaultManager] copyItemAtPath:imagePath toPath:newPath error:&error] )							{								resultPath = newPath;								NSLog( @"Copied source image from UIImagePickerControllerImageURL" );							}							else								NSLog( @"Error copying image: %@", error );						}						else							NSLog( @"Error deleting existing image: %@", error );					}				}								if( resultPath == nil )					asset = info[UIImagePickerControllerPHAsset];			}#endif						if( resultPath == nil && !simpleMediaPickMode )			{				if( asset == nil )				{					NSURL *mediaUrl = info[UIImagePickerControllerReferenceURL] ?: info[UIImagePickerControllerMediaURL];					if( mediaUrl != nil )						asset = [[PHAsset fetchAssetsWithALAssetURLs:[NSArray arrayWithObject:mediaUrl] options:nil] firstObject];				}								resultPath = [self trySavePHAsset:asset atIndex:1];			}		}				if( resultPath == nil )		{			// Save image as PNG			UIImage *image = info[UIImagePickerControllerOriginalImage];			if( image != nil )			{				resultPath = [pickedMediaSavePath stringByAppendingPathExtension:@"png"];				if( ![self saveImageAsPNG:image toPath:resultPath] )				{					NSLog( @"Error creating PNG image" );					resultPath = nil;				}			}			else				NSLog( @"Error fetching original image from picker" );		}	}	else if( CHECK_IOS_VERSION( @"9.1" ) && [info[UIImagePickerControllerMediaType] isEqualToString:(NSString *)kUTTypeLivePhoto] )	{		NSLog( @"Picked a live photo" );				// Save live photo as PNG		UIImage *image = info[UIImagePickerControllerOriginalImage];		if( image != nil )		{			resultPath = [pickedMediaSavePath stringByAppendingPathExtension:@"png"];			if( ![self saveImageAsPNG:image toPath:resultPath] )			{				NSLog( @"Error creating PNG image" );				resultPath = nil;			}		}		else			NSLog( @"Error fetching live photo's still image from picker" );	}	else	{		NSLog( @"Picked a video" );				NSURL *mediaUrl = info[UIImagePickerControllerMediaURL] ?: info[UIImagePickerControllerReferenceURL];		if( mediaUrl != nil )		{			resultPath = [mediaUrl path];						// On iOS 13, picked file becomes unreachable as soon as the UIImagePickerController disappears,			// in that case, copy the video to a temporary location			if( CHECK_IOS_VERSION( @"13.0" ) )			{				NSError *error;				NSString *newPath = [pickedMediaSavePath stringByAppendingPathExtension:[resultPath pathExtension]];								if( ![[NSFileManager defaultManager] fileExistsAtPath:newPath] || [[NSFileManager defaultManager] removeItemAtPath:newPath error:&error] )				{					if( [[NSFileManager defaultManager] copyItemAtPath:resultPath toPath:newPath error:&error] )						resultPath = newPath;					else					{						NSLog( @"Error copying video: %@", error );						resultPath = nil;					}				}				else				{					NSLog( @"Error deleting existing video: %@", error );					resultPath = nil;				}			}		}	}		popup = nil;	imagePicker = nil;	imagePickerState = 2;	UnitySendMessage( "NGMediaReceiveCallbackiOS", "OnMediaReceived", [self getCString:resultPath] );		[picker dismissViewControllerAnimated:NO completion:nil];}#pragma clang diagnostic pop#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000// Credit: https://ikyle.me/blog/2020/phpickerviewcontroller+(void)picker:(PHPickerViewController *)picker didFinishPicking:(NSArray<PHPickerResult *> *)results{	imagePickerNew = nil;	imagePickerState = 2;		[picker dismissViewControllerAnimated:NO completion:nil];		if( results != nil && [results count] > 0 )	{		NSMutableArray<NSString *> *resultPaths = [NSMutableArray arrayWithCapacity:[results count]];		NSLock *arrayLock = [[NSLock alloc] init];		dispatch_group_t group = dispatch_group_create();				for( int i = 0; i < [results count]; i++ )		{			PHPickerResult *result = results[i];			NSItemProvider *itemProvider = result.itemProvider;			NSString *assetIdentifier = result.assetIdentifier;			__block NSString *resultPath = nil;						int j = i + 1;						//NSLog( @"result: %@", result );			//NSLog( @"%@", result.assetIdentifier);			//NSLog( @"%@", result.itemProvider);			if( [itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage] )			{				NSLog( @"Picked an image" );								if( !simpleMediaPickMode && assetIdentifier != nil )				{					PHAsset *asset = [[PHAsset fetchAssetsWithLocalIdentifiers:[NSArray arrayWithObject:assetIdentifier] options:nil] firstObject];					resultPath = [self trySavePHAsset:asset atIndex:j];				}								if( resultPath != nil )				{					[arrayLock lock];					[resultPaths addObject:resultPath];					[arrayLock unlock];				}				else				{					dispatch_group_enter( group );										[itemProvider loadFileRepresentationForTypeIdentifier:(NSString *)kUTTypeImage completionHandler:^( NSURL *url, NSError *error )					{						if( url != nil )						{							// Copy the image to a temporary location because the returned image will be deleted by the OS after this callback is completed							resultPath = [url path];							NSString *newPath = [[NSString stringWithFormat:@"%@%d", pickedMediaSavePath, j] stringByAppendingPathExtension:[resultPath pathExtension]];														if( ![[NSFileManager defaultManager] fileExistsAtPath:newPath] || [[NSFileManager defaultManager] removeItemAtPath:newPath error:&error] )							{								if( [[NSFileManager defaultManager] copyItemAtPath:resultPath toPath:newPath error:&error])									resultPath = newPath;								else								{									NSLog( @"Error copying image: %@", error );									resultPath = nil;								}							}							else							{								NSLog( @"Error deleting existing image: %@", error );								resultPath = nil;							}						}						else							NSLog( @"Error getting the picked image's path: %@", error );												if( resultPath != nil )						{							[arrayLock lock];							[resultPaths addObject:resultPath];							[arrayLock unlock];						}						else						{							if( [itemProvider canLoadObjectOfClass:[UIImage class]] )							{								dispatch_group_enter( group );																[itemProvider loadObjectOfClass:[UIImage class] completionHandler:^( __kindof id<NSItemProviderReading> object, NSError *error )								{									if( object != nil && [object isKindOfClass:[UIImage class]] )									{										resultPath = [[NSString stringWithFormat:@"%@%d", pickedMediaSavePath, j] stringByAppendingPathExtension:@"png"];										if( ![self saveImageAsPNG:(UIImage *)object toPath:resultPath] )										{											NSLog( @"Error creating PNG image" );											resultPath = nil;										}									}									else										NSLog( @"Error generating UIImage from picked image: %@", error );																		[arrayLock lock];									[resultPaths addObject:( resultPath != nil ? resultPath : @"" )];									[arrayLock unlock];																		dispatch_group_leave( group );								}];							}							else							{								NSLog( @"Can't generate UIImage from picked image" );																[arrayLock lock];								[resultPaths addObject:@""];								[arrayLock unlock];							}						}												dispatch_group_leave( group );					}];				}			}			else if( CHECK_IOS_VERSION( @"9.1" ) && [itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeLivePhoto] )			{				NSLog( @"Picked a live photo" );								if( [itemProvider canLoadObjectOfClass:[UIImage class]] )				{					dispatch_group_enter( group );										[itemProvider loadObjectOfClass:[UIImage class] completionHandler:^( __kindof id<NSItemProviderReading> object, NSError *error )					{						if( object != nil && [object isKindOfClass:[UIImage class]] )						{							resultPath = [[NSString stringWithFormat:@"%@%d", pickedMediaSavePath, j] stringByAppendingPathExtension:@"png"];							if( ![self saveImageAsPNG:(UIImage *)object toPath:resultPath] )							{								NSLog( @"Error creating PNG image" );								resultPath = nil;							}						}						else							NSLog( @"Error generating UIImage from picked live photo: %@", error );												[arrayLock lock];						[resultPaths addObject:( resultPath != nil ? resultPath : @"" )];						[arrayLock unlock];												dispatch_group_leave( group );					}];				}				else if( [itemProvider canLoadObjectOfClass:[PHLivePhoto class]] )				{					dispatch_group_enter( group );										[itemProvider loadObjectOfClass:[PHLivePhoto class] completionHandler:^( __kindof id<NSItemProviderReading> object, NSError *error )					{						if( object != nil && [object isKindOfClass:[PHLivePhoto class]] )						{							// Extract image data from live photo							// Credit: https://stackoverflow.com/a/41341675/2373034							NSArray<PHAssetResource*>* livePhotoResources = [PHAssetResource assetResourcesForLivePhoto:(PHLivePhoto *)object];														PHAssetResource *livePhotoImage = nil;							for( int k = 0; k < [livePhotoResources count]; k++ )							{								if( livePhotoResources[k].type == PHAssetResourceTypePhoto )								{									livePhotoImage = livePhotoResources[k];									break;								}							}														if( livePhotoImage == nil )							{								NSLog( @"Error extracting image data from live photo" );															[arrayLock lock];								[resultPaths addObject:@""];								[arrayLock unlock];							}							else							{								dispatch_group_enter( group );																NSString *originalFilename = livePhotoImage.originalFilename;								if( originalFilename == nil || [originalFilename length] == 0 )									resultPath = [NSString stringWithFormat:@"%@%d", pickedMediaSavePath, j];								else									resultPath = [[NSString stringWithFormat:@"%@%d", pickedMediaSavePath, j] stringByAppendingPathExtension:[originalFilename pathExtension]];																[[PHAssetResourceManager defaultManager] writeDataForAssetResource:livePhotoImage toFile:[NSURL fileURLWithPath:resultPath] options:nil completionHandler:^( NSError * _Nullable error2 )								{									if( error2 != nil )									{										NSLog( @"Error saving image data from live photo: %@", error2 );										resultPath = nil;									}																		[arrayLock lock];									[resultPaths addObject:( resultPath != nil ? resultPath : @"" )];									[arrayLock unlock];																		dispatch_group_leave( group );								}];							}						}						else						{							NSLog( @"Error generating PHLivePhoto from picked live photo: %@", error );													[arrayLock lock];							[resultPaths addObject:@""];							[arrayLock unlock];						}												dispatch_group_leave( group );					}];				}				else				{					NSLog( @"Can't convert picked live photo to still image" );										[arrayLock lock];					[resultPaths addObject:@""];					[arrayLock unlock];				}			}			else if( [itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeMovie] || [itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeVideo] )			{				NSLog( @"Picked a video" );								// Get the video file's path				dispatch_group_enter( group );								[itemProvider loadFileRepresentationForTypeIdentifier:([itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeMovie] ? (NSString *)kUTTypeMovie : (NSString *)kUTTypeVideo) completionHandler:^( NSURL *url, NSError *error )				{					if( url != nil )					{						// Copy the video to a temporary location because the returned video will be deleted by the OS after this callback is completed						resultPath = [url path];						NSString *newPath = [[NSString stringWithFormat:@"%@%d", pickedMediaSavePath, j] stringByAppendingPathExtension:[resultPath pathExtension]];												if( ![[NSFileManager defaultManager] fileExistsAtPath:newPath] || [[NSFileManager defaultManager] removeItemAtPath:newPath error:&error] )						{							if( [[NSFileManager defaultManager] copyItemAtPath:resultPath toPath:newPath error:&error])								resultPath = newPath;							else							{								NSLog( @"Error copying video: %@", error );								resultPath = nil;							}						}						else						{							NSLog( @"Error deleting existing video: %@", error );							resultPath = nil;						}					}					else						NSLog( @"Error getting the picked video's path: %@", error );										[arrayLock lock];					[resultPaths addObject:( resultPath != nil ? resultPath : @"" )];					[arrayLock unlock];										dispatch_group_leave( group );				}];			}			else			{				// Unknown media type picked?				NSLog( @"Couldn't determine type of picked media: %@", itemProvider );								[arrayLock lock];				[resultPaths addObject:@""];				[arrayLock unlock];			}		}				dispatch_group_notify( group, dispatch_get_main_queue(),		^{			if( !pickingMultipleFiles )				UnitySendMessage( "NGMediaReceiveCallbackiOS", "OnMediaReceived", [self getCString:resultPaths[0]] );			else				UnitySendMessage( "NGMediaReceiveCallbackiOS", "OnMultipleMediaReceived", [self getCString:[resultPaths componentsJoinedByString:@">"]] );		});	}	else	{		NSLog( @"No media picked" );				if( !pickingMultipleFiles )			UnitySendMessage( "NGMediaReceiveCallbackiOS", "OnMediaReceived", "" );		else			UnitySendMessage( "NGMediaReceiveCallbackiOS", "OnMultipleMediaReceived", "" );	}}#endif+ (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker{	NSLog( @"UIImagePickerController cancelled" );	popup = nil;	imagePicker = nil;	UnitySendMessage( "NGMediaReceiveCallbackiOS", "OnMediaReceived", "" );		[picker dismissViewControllerAnimated:NO completion:nil];}+ (void)popoverControllerDidDismissPopover:(UIPopoverController *)popoverController{	NSLog( @"UIPopoverController dismissed" );	popup = nil;	imagePicker = nil;	UnitySendMessage( "NGMediaReceiveCallbackiOS", "OnMediaReceived", "" );}+ (NSString *)trySavePHAsset:(PHAsset *)asset atIndex:(int)filenameIndex{	if( asset == nil )		return nil;		__block NSString *resultPath = nil;		PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init];	options.synchronous = YES;	options.version = PHImageRequestOptionsVersionCurrent;	#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000	if( CHECK_IOS_VERSION( @"13.0" ) )	{		[[PHImageManager defaultManager] requestImageDataAndOrientationForAsset:asset options:options resultHandler:^( NSData *imageData, NSString *dataUTI, CGImagePropertyOrientation orientation, NSDictionary *imageInfo )		{			if( imageData != nil )				resultPath = [self trySaveSourceImage:imageData withInfo:imageInfo atIndex:filenameIndex];			else				NSLog( @"Couldn't fetch raw image data" );		}];	}	else #endif	{		[[PHImageManager defaultManager] requestImageDataForAsset:asset options:options resultHandler:^( NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *imageInfo )		{			if( imageData != nil )				resultPath = [self trySaveSourceImage:imageData withInfo:imageInfo atIndex:filenameIndex];			else				NSLog( @"Couldn't fetch raw image data" );		}];	}		return resultPath;}+ (NSString *)trySaveSourceImage:(NSData *)imageData withInfo:(NSDictionary *)info atIndex:(int)filenameIndex{	NSString *filePath = info[@"PHImageFileURLKey"];	if( filePath != nil ) // filePath can actually be an NSURL, convert it to NSString		filePath = [NSString stringWithFormat:@"%@", filePath];		if( filePath == nil || [filePath length] == 0 )	{		filePath = info[@"PHImageFileUTIKey"];		if( filePath != nil )			filePath = [NSString stringWithFormat:@"%@", filePath];	}		NSString *resultPath;	if( filePath == nil || [filePath length] == 0 )		resultPath = [NSString stringWithFormat:@"%@%d", pickedMediaSavePath, filenameIndex];	else		resultPath = [[NSString stringWithFormat:@"%@%d", pickedMediaSavePath, filenameIndex] stringByAppendingPathExtension:[filePath pathExtension]];		NSError *error;	if( ![[NSFileManager defaultManager] fileExistsAtPath:resultPath] || [[NSFileManager defaultManager] removeItemAtPath:resultPath error:&error] )	{		if( ![imageData writeToFile:resultPath atomically:YES] )		{			NSLog( @"Error copying source image to file" );			resultPath = nil;		}	}	else	{		NSLog( @"Error deleting existing image: %@", error );		resultPath = nil;	}		return resultPath;}// Credit: https://lists.apple.com/archives/cocoa-dev/2012/Jan/msg00052.html+ (int)getMediaTypeFromExtension:(NSString *)extension{	CFStringRef fileUTI = UTTypeCreatePreferredIdentifierForTag( kUTTagClassFilenameExtension, (__bridge CFStringRef) extension, NULL );		// mediaType is a bitmask:	// 1: image	// 2: video	// 4: audio (not supported)	int result = 0;	if( UTTypeConformsTo( fileUTI, kUTTypeImage ) )		result = 1;	else if( CHECK_IOS_VERSION( @"9.1" ) && UTTypeConformsTo( fileUTI, kUTTypeLivePhoto ) )		result = 1;	else if( UTTypeConformsTo( fileUTI, kUTTypeMovie ) || UTTypeConformsTo( fileUTI, kUTTypeVideo ) )		result = 2;	else if( UTTypeConformsTo( fileUTI, kUTTypeAudio ) )		result = 4;		CFRelease( fileUTI );		return result;}// Credit: https://stackoverflow.com/a/4170099/2373034+ (NSArray *)getImageMetadata:(NSString *)path{	int width = 0;	int height = 0;	int orientation = -1;		CGImageSourceRef imageSource = CGImageSourceCreateWithURL( (__bridge CFURLRef) [NSURL fileURLWithPath:path], nil );	if( imageSource != nil )	{		NSDictionary *options = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:NO] forKey:(__bridge NSString *)kCGImageSourceShouldCache];		CFDictionaryRef imageProperties = CGImageSourceCopyPropertiesAtIndex( imageSource, 0, (__bridge CFDictionaryRef) options );		CFRelease( imageSource );				CGFloat widthF = 0.0f, heightF = 0.0f;		if( imageProperties != nil )		{			if( CFDictionaryContainsKey( imageProperties, kCGImagePropertyPixelWidth ) )				CFNumberGetValue( (CFNumberRef) CFDictionaryGetValue( imageProperties, kCGImagePropertyPixelWidth ), kCFNumberCGFloatType, &widthF );						if( CFDictionaryContainsKey( imageProperties, kCGImagePropertyPixelHeight ) )				CFNumberGetValue( (CFNumberRef) CFDictionaryGetValue( imageProperties, kCGImagePropertyPixelHeight ), kCFNumberCGFloatType, &heightF );						if( CFDictionaryContainsKey( imageProperties, kCGImagePropertyOrientation ) )			{				CFNumberGetValue( (CFNumberRef) CFDictionaryGetValue( imageProperties, kCGImagePropertyOrientation ), kCFNumberIntType, &orientation );								if( orientation > 4 )				{					// Landscape image					CGFloat temp = widthF;					widthF = heightF;					heightF = temp;				}			}						CFRelease( imageProperties );		}				width = (int) roundf( widthF );		height = (int) roundf( heightF );	}		return [[NSArray alloc] initWithObjects:[NSNumber numberWithInt:width], [NSNumber numberWithInt:height], [NSNumber numberWithInt:orientation], nil];}+ (char *)getImageProperties:(NSString *)path{	NSArray *metadata = [self getImageMetadata:path];		int orientationUnity;	int orientation = [metadata[2] intValue];		// To understand the magic numbers, see ImageOrientation enum in NativeGallery.cs	// and http://sylvana.net/jpegcrop/exif_orientation.html	if( orientation == 1 )		orientationUnity = 0;	else if( orientation == 2 )		orientationUnity = 4;	else if( orientation == 3 )		orientationUnity = 2;	else if( orientation == 4 )		orientationUnity = 6;	else if( orientation == 5 )		orientationUnity = 5;	else if( orientation == 6 )		orientationUnity = 1;	else if( orientation == 7 )		orientationUnity = 7;	else if( orientation == 8 )		orientationUnity = 3;	else		orientationUnity = -1;		return [self getCString:[NSString stringWithFormat:@"%d>%d> >%d", [metadata[0] intValue], [metadata[1] intValue], orientationUnity]];}+ (char *)getVideoProperties:(NSString *)path{	CGSize size = CGSizeZero;	float rotation = 0;	long long duration = 0;		AVURLAsset *asset = [AVURLAsset URLAssetWithURL:[NSURL fileURLWithPath:path] options:nil];	if( asset != nil )	{		duration = (long long) round( CMTimeGetSeconds( [asset duration] ) * 1000 );		CGAffineTransform transform = [asset preferredTransform];		NSArray<AVAssetTrack *>* videoTracks = [asset tracksWithMediaType:AVMediaTypeVideo];		if( videoTracks != nil && [videoTracks count] > 0 )		{			size = [[videoTracks objectAtIndex:0] naturalSize];			transform = [[videoTracks objectAtIndex:0] preferredTransform];		}				rotation = atan2( transform.b, transform.a ) * ( 180.0 / M_PI );	}		return [self getCString:[NSString stringWithFormat:@"%d>%d>%lld>%f", (int) roundf( size.width ), (int) roundf( size.height ), duration, rotation]];}+ (char *)getVideoThumbnail:(NSString *)path savePath:(NSString *)savePath maximumSize:(int)maximumSize captureTime:(double)captureTime{	AVAssetImageGenerator *thumbnailGenerator = [[AVAssetImageGenerator alloc] initWithAsset:[[AVURLAsset alloc] initWithURL:[NSURL fileURLWithPath:path] options:nil]];	thumbnailGenerator.appliesPreferredTrackTransform = YES;	thumbnailGenerator.maximumSize = CGSizeMake( (CGFloat) maximumSize, (CGFloat) maximumSize );	thumbnailGenerator.requestedTimeToleranceBefore = kCMTimeZero;	thumbnailGenerator.requestedTimeToleranceAfter = kCMTimeZero;		if( captureTime < 0.0 )		captureTime = 0.0;	else	{		AVURLAsset *asset = [AVURLAsset URLAssetWithURL:[NSURL fileURLWithPath:path] options:nil];		if( asset != nil )		{			double videoDuration = CMTimeGetSeconds( [asset duration] );			if( videoDuration > 0.0 && captureTime >= videoDuration - 0.1 )			{				if( captureTime > videoDuration )					captureTime = videoDuration;								thumbnailGenerator.requestedTimeToleranceBefore = CMTimeMakeWithSeconds( 1.0, 600 );			}		}	}		NSError *error = nil;	CGImageRef image = [thumbnailGenerator copyCGImageAtTime:CMTimeMakeWithSeconds( captureTime, 600 ) actualTime:nil error:&error];	if( image == nil )	{		if( error != nil )			NSLog( @"Error generating video thumbnail: %@", error );		else			NSLog( @"Error generating video thumbnail..." );				return [self getCString:@""];	}		UIImage *thumbnail = [[UIImage alloc] initWithCGImage:image];	CGImageRelease( image );		if( ![UIImagePNGRepresentation( thumbnail ) writeToFile:savePath atomically:YES] )	{		NSLog( @"Error saving thumbnail image" );		return [self getCString:@""];	}		return [self getCString:savePath];}+ (BOOL)saveImageAsPNG:(UIImage *)image toPath:(NSString *)resultPath{	return [UIImagePNGRepresentation( [self scaleImage:image maxSize:16384] ) writeToFile:resultPath atomically:YES];}+ (UIImage *)scaleImage:(UIImage *)image maxSize:(int)maxSize{	CGFloat width = image.size.width;	CGFloat height = image.size.height;		UIImageOrientation orientation = image.imageOrientation;	if( width <= maxSize && height <= maxSize && orientation != UIImageOrientationDown &&		orientation != UIImageOrientationLeft && orientation != UIImageOrientationRight &&		orientation != UIImageOrientationLeftMirrored && orientation != UIImageOrientationRightMirrored &&		orientation != UIImageOrientationUpMirrored && orientation != UIImageOrientationDownMirrored )		return image;		CGFloat scaleX = 1.0f;	CGFloat scaleY = 1.0f;	if( width > maxSize )		scaleX = maxSize / width;	if( height > maxSize )		scaleY = maxSize / height;		// Credit: https://github.com/mbcharbonneau/UIImage-Categories/blob/master/UIImage%2BAlpha.m	CGImageAlphaInfo alpha = CGImageGetAlphaInfo( image.CGImage );	BOOL hasAlpha = alpha == kCGImageAlphaFirst || alpha == kCGImageAlphaLast || alpha == kCGImageAlphaPremultipliedFirst || alpha == kCGImageAlphaPremultipliedLast;		CGFloat scaleRatio = scaleX < scaleY ? scaleX : scaleY;	CGRect imageRect = CGRectMake( 0, 0, width * scaleRatio, height * scaleRatio );	#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 100000	// Resize image with UIGraphicsImageRenderer (Apple's recommended API) if possible	if( CHECK_IOS_VERSION( @"10.0" ) )	{		UIGraphicsImageRendererFormat *format = [image imageRendererFormat];		format.opaque = !hasAlpha;		format.scale = image.scale;	   		UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:imageRect.size format:format];		image = [renderer imageWithActions:^( UIGraphicsImageRendererContext* _Nonnull myContext )		{			[image drawInRect:imageRect];		}];	}	else	#endif	{		UIGraphicsBeginImageContextWithOptions( imageRect.size, !hasAlpha, image.scale );		[image drawInRect:imageRect];		image = UIGraphicsGetImageFromCurrentImageContext();		UIGraphicsEndImageContext();	}		return image;}+ (char *)loadImageAtPath:(NSString *)path tempFilePath:(NSString *)tempFilePath maximumSize:(int)maximumSize{	// Check if the image can be loaded by Unity without requiring a conversion to PNG	// Credit: https://stackoverflow.com/a/12048937/2373034	NSString *extension = [path pathExtension];	BOOL conversionNeeded = [extension caseInsensitiveCompare:@"jpg"] != NSOrderedSame && [extension caseInsensitiveCompare:@"jpeg"] != NSOrderedSame && [extension caseInsensitiveCompare:@"png"] != NSOrderedSame;	if( !conversionNeeded )	{		// Check if the image needs to be processed at all		NSArray *metadata = [self getImageMetadata:path];		int orientationInt = [metadata[2] intValue];  // 1: correct orientation, [1,8]: valid orientation range		if( orientationInt == 1 && [metadata[0] intValue] <= maximumSize && [metadata[1] intValue] <= maximumSize )			return [self getCString:path];	}		UIImage *image = [UIImage imageWithContentsOfFile:path];	if( image == nil )		return [self getCString:path];		UIImage *scaledImage = [self scaleImage:image maxSize:maximumSize];	if( conversionNeeded || scaledImage != image )	{		if( ![UIImagePNGRepresentation( scaledImage ) writeToFile:tempFilePath atomically:YES] )		{			NSLog( @"Error creating scaled image" );			return [self getCString:path];		}				return [self getCString:tempFilePath];	}	else		return [self getCString:path];}// Credit: https://stackoverflow.com/a/37052118/2373034+ (char *)getCString:(NSString *)source{	if( source == nil )		source = @"";		const char *sourceUTF8 = [source UTF8String];	char *result = (char*) malloc( strlen( sourceUTF8 ) + 1 );	strcpy( result, sourceUTF8 );		return result;}@endextern "C" int _NativeGallery_CheckPermission( int readPermission, int permissionFreeMode ){	return [UNativeGallery checkPermission:( readPermission == 1 ) permissionFreeMode:( permissionFreeMode == 1 )];}extern "C" int _NativeGallery_RequestPermission( int readPermission, int permissionFreeMode, int asyncMode ){	return [UNativeGallery requestPermission:( readPermission == 1 ) permissionFreeMode:( permissionFreeMode == 1 ) asyncMode:( asyncMode == 1 )];}extern "C" void _NativeGallery_ShowLimitedLibraryPicker(){	return [UNativeGallery showLimitedLibraryPicker];}extern "C" int _NativeGallery_CanOpenSettings(){	return [UNativeGallery canOpenSettings];}extern "C" void _NativeGallery_OpenSettings(){	[UNativeGallery openSettings];}extern "C" int _NativeGallery_CanPickMultipleMedia(){	return [UNativeGallery canPickMultipleMedia];}extern "C" void _NativeGallery_ImageWriteToAlbum( const char* path, const char* album, int permissionFreeMode ){	[UNativeGallery saveMedia:[NSString stringWithUTF8String:path] albumName:[NSString stringWithUTF8String:album] isImg:YES permissionFreeMode:( permissionFreeMode == 1 )];}extern "C" void _NativeGallery_VideoWriteToAlbum( const char* path, const char* album, int permissionFreeMode ){	[UNativeGallery saveMedia:[NSString stringWithUTF8String:path] albumName:[NSString stringWithUTF8String:album] isImg:NO permissionFreeMode:( permissionFreeMode == 1 )];}extern "C" void _NativeGallery_PickMedia( const char* mediaSavePath, int mediaType, int permissionFreeMode, int selectionLimit ){	[UNativeGallery pickMedia:mediaType savePath:[NSString stringWithUTF8String:mediaSavePath] permissionFreeMode:( permissionFreeMode == 1 ) selectionLimit:selectionLimit];}extern "C" int _NativeGallery_IsMediaPickerBusy(){	return [UNativeGallery isMediaPickerBusy];}extern "C" int _NativeGallery_GetMediaTypeFromExtension( const char* extension ){	return [UNativeGallery getMediaTypeFromExtension:[NSString stringWithUTF8String:extension]];}extern "C" char* _NativeGallery_GetImageProperties( const char* path ){	return [UNativeGallery getImageProperties:[NSString stringWithUTF8String:path]];}extern "C" char* _NativeGallery_GetVideoProperties( const char* path ){	return [UNativeGallery getVideoProperties:[NSString stringWithUTF8String:path]];}extern "C" char* _NativeGallery_GetVideoThumbnail( const char* path, const char* thumbnailSavePath, int maxSize, double captureTimeInSeconds ){	return [UNativeGallery getVideoThumbnail:[NSString stringWithUTF8String:path] savePath:[NSString stringWithUTF8String:thumbnailSavePath] maximumSize:maxSize captureTime:captureTimeInSeconds];}extern "C" char* _NativeGallery_LoadImageAtPath( const char* path, const char* temporaryFilePath, int maxSize ){	return [UNativeGallery loadImageAtPath:[NSString stringWithUTF8String:path] tempFilePath:[NSString stringWithUTF8String:temporaryFilePath] maximumSize:maxSize];}
 |