NativeGallery.mm 53 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540
  1. #import <Foundation/Foundation.h>
  2. #import <Photos/Photos.h>
  3. #import <MobileCoreServices/UTCoreTypes.h>
  4. #import <MobileCoreServices/MobileCoreServices.h>
  5. #import <ImageIO/ImageIO.h>
  6. #if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000
  7. #import <AssetsLibrary/AssetsLibrary.h>
  8. #endif
  9. #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000
  10. #import <PhotosUI/PhotosUI.h>
  11. #endif
  12. #ifdef UNITY_4_0 || UNITY_5_0
  13. #import "iPhone_View.h"
  14. #else
  15. extern UIViewController* UnityGetGLViewController();
  16. #endif
  17. #define CHECK_IOS_VERSION( version ) ([[[UIDevice currentDevice] systemVersion] compare:version options:NSNumericSearch] != NSOrderedAscending)
  18. @interface UNativeGallery:NSObject
  19. + (int)checkPermission:(BOOL)readPermission permissionFreeMode:(BOOL)permissionFreeMode;
  20. + (int)requestPermission:(BOOL)readPermission permissionFreeMode:(BOOL)permissionFreeMode;
  21. + (void)showLimitedLibraryPicker;
  22. + (int)canOpenSettings;
  23. + (void)openSettings;
  24. + (int)canPickMultipleMedia;
  25. + (void)saveMedia:(NSString *)path albumName:(NSString *)album isImg:(BOOL)isImg permissionFreeMode:(BOOL)permissionFreeMode;
  26. + (void)pickMedia:(int)mediaType savePath:(NSString *)mediaSavePath permissionFreeMode:(BOOL)permissionFreeMode selectionLimit:(int)selectionLimit;
  27. + (int)isMediaPickerBusy;
  28. + (int)getMediaTypeFromExtension:(NSString *)extension;
  29. + (char *)getImageProperties:(NSString *)path;
  30. + (char *)getVideoProperties:(NSString *)path;
  31. + (char *)getVideoThumbnail:(NSString *)path savePath:(NSString *)savePath maximumSize:(int)maximumSize captureTime:(double)captureTime;
  32. + (char *)loadImageAtPath:(NSString *)path tempFilePath:(NSString *)tempFilePath maximumSize:(int)maximumSize;
  33. @end
  34. @implementation UNativeGallery
  35. static NSString *pickedMediaSavePath;
  36. static UIPopoverController *popup;
  37. static UIImagePickerController *imagePicker;
  38. #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000
  39. static PHPickerViewController *imagePickerNew;
  40. #endif
  41. static int imagePickerState = 0; // 0 -> none, 1 -> showing (always in this state on iPad), 2 -> finished
  42. static BOOL simpleMediaPickMode;
  43. static BOOL pickingMultipleFiles = NO;
  44. #pragma clang diagnostic push
  45. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  46. + (int)checkPermission:(BOOL)readPermission permissionFreeMode:(BOOL)permissionFreeMode
  47. {
  48. #if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000
  49. if( CHECK_IOS_VERSION( @"8.0" ) )
  50. {
  51. #endif
  52. // version >= iOS 8: check permission using Photos framework
  53. // On iOS 11 and later, permission isn't mandatory to fetch media from Photos
  54. if( readPermission && permissionFreeMode && CHECK_IOS_VERSION( @"11.0" ) )
  55. return 1;
  56. #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000
  57. // Photos permissions has changed on iOS 14
  58. if( CHECK_IOS_VERSION( @"14.0" ) )
  59. {
  60. // Request ReadWrite permission in 2 cases:
  61. // 1) When attempting to pick media from Photos with PHPhotoLibrary (readPermission=true and permissionFreeMode=false)
  62. // 2) When attempting to write media to a specific album in Photos using PHPhotoLibrary (readPermission=false and permissionFreeMode=false)
  63. PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatusForAccessLevel:( ( readPermission || !permissionFreeMode ) ? PHAccessLevelReadWrite : PHAccessLevelAddOnly )];
  64. if( status == PHAuthorizationStatusAuthorized )
  65. return 1;
  66. else if( status == PHAuthorizationStatusRestricted )
  67. return 3;
  68. else if( status == PHAuthorizationStatusNotDetermined )
  69. return 2;
  70. else
  71. return 0;
  72. }
  73. else
  74. #endif
  75. {
  76. PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus];
  77. if( status == PHAuthorizationStatusAuthorized )
  78. return 1;
  79. else if( status == PHAuthorizationStatusNotDetermined )
  80. return 2;
  81. else
  82. return 0;
  83. }
  84. #if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000
  85. }
  86. else
  87. {
  88. // version < iOS 8: check permission using AssetsLibrary framework (Photos framework not available)
  89. ALAuthorizationStatus status = [ALAssetsLibrary authorizationStatus];
  90. if( status == ALAuthorizationStatusAuthorized )
  91. return 1;
  92. else if( status == ALAuthorizationStatusNotDetermined )
  93. return 2;
  94. else
  95. return 0;
  96. }
  97. #endif
  98. }
  99. #pragma clang diagnostic pop
  100. + (int)requestPermission:(BOOL)readPermission permissionFreeMode:(BOOL)permissionFreeMode
  101. {
  102. #if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000
  103. if( CHECK_IOS_VERSION( @"8.0" ) )
  104. {
  105. #endif
  106. // version >= iOS 8: request permission using Photos framework
  107. // On iOS 11 and later, permission isn't mandatory to fetch media from Photos
  108. if( readPermission && permissionFreeMode && CHECK_IOS_VERSION( @"11.0" ) )
  109. return 1;
  110. #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000
  111. if( CHECK_IOS_VERSION( @"14.0" ) )
  112. {
  113. // Photos permissions has changed on iOS 14. There are 2 permission dialogs now:
  114. // - AddOnly permission dialog: has 2 options: "Allow" and "Don't Allow". This dialog grants permission for save operations only. Unfortunately,
  115. // saving media to a custom album isn't possible with this dialog, media can only be saved to the default Photos album
  116. // - ReadWrite permission dialog: has 3 options: "Allow Access to All Photos" (i.e. full permission), "Select Photos" (i.e. limited access) and
  117. // "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,
  118. // this dialog will be used if PermissionFreeMode is set to false. So, PermissionFreeMode determines whether or not saving to a custom album is
  119. // be supported
  120. return [self requestPermissionNewest:( readPermission || !permissionFreeMode )];
  121. }
  122. else
  123. #endif
  124. return [self requestPermissionNew];
  125. #if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000
  126. }
  127. else
  128. {
  129. // version < iOS 8: request permission using AssetsLibrary framework (Photos framework not available)
  130. return [self requestPermissionOld];
  131. }
  132. #endif
  133. }
  134. #if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000
  135. // Credit: https://stackoverflow.com/a/26933380/2373034
  136. #pragma clang diagnostic push
  137. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  138. + (int)requestPermissionOld
  139. {
  140. ALAuthorizationStatus status = [ALAssetsLibrary authorizationStatus];
  141. if( status == ALAuthorizationStatusAuthorized )
  142. return 1;
  143. else if( status == ALAuthorizationStatusNotDetermined )
  144. {
  145. __block BOOL authorized = NO;
  146. ALAssetsLibrary *lib = [[ALAssetsLibrary alloc] init];
  147. dispatch_semaphore_t sema = dispatch_semaphore_create( 0 );
  148. [lib enumerateGroupsWithTypes:ALAssetsGroupAll usingBlock:^( ALAssetsGroup *group, BOOL *stop )
  149. {
  150. *stop = YES;
  151. authorized = YES;
  152. dispatch_semaphore_signal( sema );
  153. }
  154. failureBlock:^( NSError *error )
  155. {
  156. dispatch_semaphore_signal( sema );
  157. }];
  158. dispatch_semaphore_wait( sema, DISPATCH_TIME_FOREVER );
  159. return authorized ? 1 : 0;
  160. }
  161. return 0;
  162. }
  163. #pragma clang diagnostic pop
  164. #endif
  165. // Credit: https://stackoverflow.com/a/32989022/2373034
  166. + (int)requestPermissionNew
  167. {
  168. PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus];
  169. if( status == PHAuthorizationStatusAuthorized )
  170. return 1;
  171. else if( status == PHAuthorizationStatusNotDetermined )
  172. {
  173. __block BOOL authorized = NO;
  174. dispatch_semaphore_t sema = dispatch_semaphore_create( 0 );
  175. [PHPhotoLibrary requestAuthorization:^( PHAuthorizationStatus status )
  176. {
  177. authorized = ( status == PHAuthorizationStatusAuthorized );
  178. dispatch_semaphore_signal( sema );
  179. }];
  180. dispatch_semaphore_wait( sema, DISPATCH_TIME_FOREVER );
  181. return authorized ? 1 : 0;
  182. }
  183. return 0;
  184. }
  185. #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000
  186. + (int)requestPermissionNewest:(BOOL)readPermission
  187. {
  188. PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatusForAccessLevel:( readPermission ? PHAccessLevelReadWrite : PHAccessLevelAddOnly )];
  189. if( status == PHAuthorizationStatusAuthorized )
  190. return 1;
  191. else if( status == PHAuthorizationStatusRestricted )
  192. return 3;
  193. else if( status == PHAuthorizationStatusNotDetermined )
  194. {
  195. __block int authorized = 0;
  196. dispatch_semaphore_t sema = dispatch_semaphore_create( 0 );
  197. [PHPhotoLibrary requestAuthorizationForAccessLevel:( readPermission ? PHAccessLevelReadWrite : PHAccessLevelAddOnly ) handler:^( PHAuthorizationStatus status )
  198. {
  199. if( status == PHAuthorizationStatusAuthorized )
  200. authorized = 1;
  201. else if( status == PHAuthorizationStatusRestricted )
  202. authorized = 3;
  203. dispatch_semaphore_signal( sema );
  204. }];
  205. dispatch_semaphore_wait( sema, DISPATCH_TIME_FOREVER );
  206. return authorized;
  207. }
  208. return 0;
  209. }
  210. #endif
  211. // When Photos permission is set to restricted, allows user to change the permission or change the list of restricted images
  212. // It doesn't support a deterministic callback; for example there is a photoLibraryDidChange event but it won't be invoked if
  213. // user doesn't change the list of restricted images
  214. + (void)showLimitedLibraryPicker
  215. {
  216. #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000
  217. PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatusForAccessLevel:PHAccessLevelReadWrite];
  218. if( status == PHAuthorizationStatusNotDetermined )
  219. [self requestPermissionNewest:YES];
  220. else if( status == PHAuthorizationStatusRestricted )
  221. [[PHPhotoLibrary sharedPhotoLibrary] presentLimitedLibraryPickerFromViewController:UnityGetGLViewController()];
  222. #endif
  223. }
  224. // Credit: https://stackoverflow.com/a/25453667/2373034
  225. + (int)canOpenSettings
  226. {
  227. return ( &UIApplicationOpenSettingsURLString != NULL ) ? 1 : 0;
  228. }
  229. // Credit: https://stackoverflow.com/a/25453667/2373034
  230. #pragma clang diagnostic push
  231. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  232. + (void)openSettings
  233. {
  234. if( &UIApplicationOpenSettingsURLString != NULL )
  235. {
  236. #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 100000
  237. if( CHECK_IOS_VERSION( @"10.0" ) )
  238. [[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString] options:@{} completionHandler:nil];
  239. else
  240. #endif
  241. [[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]];
  242. }
  243. }
  244. #pragma clang diagnostic pop
  245. + (int)canPickMultipleMedia
  246. {
  247. #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000
  248. if( CHECK_IOS_VERSION( @"14.0" ) )
  249. return 1;
  250. else
  251. #endif
  252. return 0;
  253. }
  254. + (void)saveMedia:(NSString *)path albumName:(NSString *)album isImg:(BOOL)isImg permissionFreeMode:(BOOL)permissionFreeMode
  255. {
  256. #if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000
  257. if( CHECK_IOS_VERSION( @"8.0" ) )
  258. {
  259. #endif
  260. // version >= iOS 8: save to specified album using Photos framework
  261. // On iOS 14+, permission workflow has changed significantly with the addition of PHAuthorizationStatusRestricted permission. On those versions,
  262. // user must grant Full Photos permission to be able to save to a custom album. Hence, there are 2 workflows:
  263. // - If PermissionFreeMode is enabled, save the media directly to the default album (i.e. ignore 'album' parameter). This will present a simple
  264. // 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
  265. // - Otherwise, the more complex "The app requires access to Photos to interact with it." permission dialog will be shown and if the user grants
  266. // Full Photos permission, only then the image will be saved to the specified album. If user selects "Selected Photos" permission, default album will be
  267. // used as fallback
  268. [self saveMediaNew:path albumName:album isImage:isImg saveToDefaultAlbum:( permissionFreeMode && CHECK_IOS_VERSION( @"14.0" ) )];
  269. #if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000
  270. }
  271. else
  272. {
  273. // version < iOS 8: save using AssetsLibrary framework (Photos framework not available)
  274. [self saveMediaOld:path albumName:album isImage:isImg];
  275. }
  276. #endif
  277. }
  278. #if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000
  279. // Credit: https://stackoverflow.com/a/22056664/2373034
  280. #pragma clang diagnostic push
  281. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  282. + (void)saveMediaOld:(NSString *)path albumName:(NSString *)album isImage:(BOOL)isImage
  283. {
  284. ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
  285. if( !isImage && ![library videoAtPathIsCompatibleWithSavedPhotosAlbum:[NSURL fileURLWithPath:path]])
  286. {
  287. NSLog( @"Error saving video: Video format is not compatible with Photos" );
  288. [[NSFileManager defaultManager] removeItemAtPath:path error:nil];
  289. UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" );
  290. return;
  291. }
  292. void (^saveBlock)(ALAssetsGroup *assetCollection) = ^void( ALAssetsGroup *assetCollection )
  293. {
  294. void (^saveResultBlock)(NSURL *assetURL, NSError *error) = ^void( NSURL *assetURL, NSError *error )
  295. {
  296. [[NSFileManager defaultManager] removeItemAtPath:path error:nil];
  297. if( error.code == 0 )
  298. {
  299. [library assetForURL:assetURL resultBlock:^( ALAsset *asset )
  300. {
  301. [assetCollection addAsset:asset];
  302. UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveCompleted", "" );
  303. }
  304. failureBlock:^( NSError* error )
  305. {
  306. NSLog( @"Error moving asset to album: %@", error );
  307. UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" );
  308. }];
  309. }
  310. else
  311. {
  312. NSLog( @"Error creating asset: %@", error );
  313. UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" );
  314. }
  315. };
  316. if( !isImage )
  317. [library writeImageDataToSavedPhotosAlbum:[NSData dataWithContentsOfFile:path] metadata:nil completionBlock:saveResultBlock];
  318. else
  319. [library writeVideoAtPathToSavedPhotosAlbum:[NSURL fileURLWithPath:path] completionBlock:saveResultBlock];
  320. };
  321. __block BOOL albumFound = NO;
  322. [library enumerateGroupsWithTypes:ALAssetsGroupAlbum usingBlock:^( ALAssetsGroup *group, BOOL *stop )
  323. {
  324. if( [[group valueForProperty:ALAssetsGroupPropertyName] isEqualToString:album] )
  325. {
  326. *stop = YES;
  327. albumFound = YES;
  328. saveBlock( group );
  329. }
  330. else if( group == nil && albumFound==NO )
  331. {
  332. // Album doesn't exist
  333. [library addAssetsGroupAlbumWithName:album resultBlock:^( ALAssetsGroup *group )
  334. {
  335. saveBlock( group );
  336. }
  337. failureBlock:^( NSError *error )
  338. {
  339. NSLog( @"Error creating album: %@", error );
  340. [[NSFileManager defaultManager] removeItemAtPath:path error:nil];
  341. UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" );
  342. }];
  343. }
  344. }
  345. failureBlock:^( NSError* error )
  346. {
  347. NSLog( @"Error listing albums: %@", error );
  348. [[NSFileManager defaultManager] removeItemAtPath:path error:nil];
  349. UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" );
  350. }];
  351. }
  352. #pragma clang diagnostic pop
  353. #endif
  354. // Credit: https://stackoverflow.com/a/39909129/2373034
  355. + (void)saveMediaNew:(NSString *)path albumName:(NSString *)album isImage:(BOOL)isImage saveToDefaultAlbum:(BOOL)saveToDefaultAlbum
  356. {
  357. void (^saveToPhotosAlbum)() = ^void()
  358. {
  359. if( isImage )
  360. {
  361. // Try preserving image metadata (essential for animated gif images)
  362. [[PHPhotoLibrary sharedPhotoLibrary] performChanges:
  363. ^{
  364. [PHAssetChangeRequest creationRequestForAssetFromImageAtFileURL:[NSURL fileURLWithPath:path]];
  365. }
  366. completionHandler:^( BOOL success, NSError *error )
  367. {
  368. if( success )
  369. {
  370. [[NSFileManager defaultManager] removeItemAtPath:path error:nil];
  371. UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveCompleted", "" );
  372. }
  373. else
  374. {
  375. NSLog( @"Error creating asset in default Photos album: %@", error );
  376. UIImage *image = [UIImage imageWithContentsOfFile:path];
  377. if( image != nil )
  378. UIImageWriteToSavedPhotosAlbum( image, self, @selector(image:didFinishSavingWithError:contextInfo:), (__bridge_retained void *) path );
  379. else
  380. {
  381. NSLog( @"Couldn't create UIImage from file at path: %@", path );
  382. [[NSFileManager defaultManager] removeItemAtPath:path error:nil];
  383. UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" );
  384. }
  385. }
  386. }];
  387. }
  388. else
  389. {
  390. if( UIVideoAtPathIsCompatibleWithSavedPhotosAlbum( path ) )
  391. UISaveVideoAtPathToSavedPhotosAlbum( path, self, @selector(video:didFinishSavingWithError:contextInfo:), (__bridge_retained void *) path );
  392. else
  393. {
  394. NSLog( @"Video at path isn't compatible with saved photos album: %@", path );
  395. [[NSFileManager defaultManager] removeItemAtPath:path error:nil];
  396. UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" );
  397. }
  398. }
  399. };
  400. void (^saveBlock)(PHAssetCollection *assetCollection) = ^void( PHAssetCollection *assetCollection )
  401. {
  402. [[PHPhotoLibrary sharedPhotoLibrary] performChanges:
  403. ^{
  404. PHAssetChangeRequest *assetChangeRequest;
  405. if( isImage )
  406. assetChangeRequest = [PHAssetChangeRequest creationRequestForAssetFromImageAtFileURL:[NSURL fileURLWithPath:path]];
  407. else
  408. assetChangeRequest = [PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:[NSURL fileURLWithPath:path]];
  409. PHAssetCollectionChangeRequest *assetCollectionChangeRequest = [PHAssetCollectionChangeRequest changeRequestForAssetCollection:assetCollection];
  410. [assetCollectionChangeRequest addAssets:@[[assetChangeRequest placeholderForCreatedAsset]]];
  411. }
  412. completionHandler:^( BOOL success, NSError *error )
  413. {
  414. if( success )
  415. {
  416. [[NSFileManager defaultManager] removeItemAtPath:path error:nil];
  417. UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveCompleted", "" );
  418. }
  419. else
  420. {
  421. NSLog( @"Error creating asset: %@", error );
  422. saveToPhotosAlbum();
  423. }
  424. }];
  425. };
  426. if( saveToDefaultAlbum )
  427. saveToPhotosAlbum();
  428. else
  429. {
  430. PHFetchOptions *fetchOptions = [[PHFetchOptions alloc] init];
  431. fetchOptions.predicate = [NSPredicate predicateWithFormat:@"localizedTitle = %@", album];
  432. PHFetchResult *fetchResult = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeAlbum subtype:PHAssetCollectionSubtypeAny options:fetchOptions];
  433. if( fetchResult.count > 0 )
  434. saveBlock( fetchResult.firstObject);
  435. else
  436. {
  437. __block PHObjectPlaceholder *albumPlaceholder;
  438. [[PHPhotoLibrary sharedPhotoLibrary] performChanges:
  439. ^{
  440. PHAssetCollectionChangeRequest *changeRequest = [PHAssetCollectionChangeRequest creationRequestForAssetCollectionWithTitle:album];
  441. albumPlaceholder = changeRequest.placeholderForCreatedAssetCollection;
  442. }
  443. completionHandler:^( BOOL success, NSError *error )
  444. {
  445. if( success )
  446. {
  447. PHFetchResult *fetchResult = [PHAssetCollection fetchAssetCollectionsWithLocalIdentifiers:@[albumPlaceholder.localIdentifier] options:nil];
  448. if( fetchResult.count > 0 )
  449. saveBlock( fetchResult.firstObject);
  450. else
  451. {
  452. NSLog( @"Error creating album: Album placeholder not found" );
  453. saveToPhotosAlbum();
  454. }
  455. }
  456. else
  457. {
  458. NSLog( @"Error creating album: %@", error );
  459. saveToPhotosAlbum();
  460. }
  461. }];
  462. }
  463. }
  464. }
  465. + (void)image:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo
  466. {
  467. NSString* path = (__bridge_transfer NSString *)(contextInfo);
  468. [[NSFileManager defaultManager] removeItemAtPath:path error:nil];
  469. if( error == nil )
  470. UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveCompleted", "" );
  471. else
  472. {
  473. NSLog( @"Error saving image with UIImageWriteToSavedPhotosAlbum: %@", error );
  474. UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" );
  475. }
  476. }
  477. + (void)video:(NSString *)videoPath didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo
  478. {
  479. NSString* path = (__bridge_transfer NSString *)(contextInfo);
  480. [[NSFileManager defaultManager] removeItemAtPath:path error:nil];
  481. if( error == nil )
  482. UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveCompleted", "" );
  483. else
  484. {
  485. NSLog( @"Error saving video with UISaveVideoAtPathToSavedPhotosAlbum: %@", error );
  486. UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" );
  487. }
  488. }
  489. // Credit: https://stackoverflow.com/a/10531752/2373034
  490. + (void)pickMedia:(int)mediaType savePath:(NSString *)mediaSavePath permissionFreeMode:(BOOL)permissionFreeMode selectionLimit:(int)selectionLimit
  491. {
  492. pickedMediaSavePath = mediaSavePath;
  493. imagePickerState = 1;
  494. simpleMediaPickMode = permissionFreeMode && CHECK_IOS_VERSION( @"11.0" );
  495. #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000
  496. if( CHECK_IOS_VERSION( @"14.0" ) )
  497. {
  498. // PHPickerViewController is used on iOS 14
  499. PHPickerConfiguration *config = simpleMediaPickMode ? [[PHPickerConfiguration alloc] init] : [[PHPickerConfiguration alloc] initWithPhotoLibrary:[PHPhotoLibrary sharedPhotoLibrary]];
  500. config.preferredAssetRepresentationMode = PHPickerConfigurationAssetRepresentationModeCurrent;
  501. config.selectionLimit = selectionLimit;
  502. pickingMultipleFiles = selectionLimit != 1;
  503. // mediaType is a bitmask:
  504. // 1: image
  505. // 2: video
  506. // 4: audio (not supported)
  507. if( mediaType == 1 )
  508. config.filter = [PHPickerFilter anyFilterMatchingSubfilters:[NSArray arrayWithObjects:[PHPickerFilter imagesFilter], [PHPickerFilter livePhotosFilter], nil]];
  509. else if( mediaType == 2 )
  510. config.filter = [PHPickerFilter videosFilter];
  511. else
  512. config.filter = [PHPickerFilter anyFilterMatchingSubfilters:[NSArray arrayWithObjects:[PHPickerFilter imagesFilter], [PHPickerFilter livePhotosFilter], [PHPickerFilter videosFilter], nil]];
  513. imagePickerNew = [[PHPickerViewController alloc] initWithConfiguration:config];
  514. imagePickerNew.delegate = (id) self;
  515. [UnityGetGLViewController() presentViewController:imagePickerNew animated:YES completion:^{ imagePickerState = 0; }];
  516. }
  517. else
  518. #endif
  519. {
  520. // UIImagePickerController is used on previous versions
  521. imagePicker = [[UIImagePickerController alloc] init];
  522. imagePicker.delegate = (id) self;
  523. imagePicker.allowsEditing = NO;
  524. imagePicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
  525. // mediaType is a bitmask:
  526. // 1: image
  527. // 2: video
  528. // 4: audio (not supported)
  529. if( mediaType == 1 )
  530. {
  531. if( CHECK_IOS_VERSION( @"9.1" ) )
  532. imagePicker.mediaTypes = [NSArray arrayWithObjects:(NSString *)kUTTypeImage, (NSString *)kUTTypeLivePhoto, nil];
  533. else
  534. imagePicker.mediaTypes = [NSArray arrayWithObject:(NSString *)kUTTypeImage];
  535. }
  536. else if( mediaType == 2 )
  537. imagePicker.mediaTypes = [NSArray arrayWithObjects:(NSString *)kUTTypeMovie, (NSString *)kUTTypeVideo, nil];
  538. else
  539. {
  540. if( CHECK_IOS_VERSION( @"9.1" ) )
  541. imagePicker.mediaTypes = [NSArray arrayWithObjects:(NSString *)kUTTypeImage, (NSString *)kUTTypeLivePhoto, (NSString *)kUTTypeMovie, (NSString *)kUTTypeVideo, nil];
  542. else
  543. imagePicker.mediaTypes = [NSArray arrayWithObjects:(NSString *)kUTTypeImage, (NSString *)kUTTypeMovie, (NSString *)kUTTypeVideo, nil];
  544. }
  545. #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000
  546. if( mediaType != 1 )
  547. {
  548. // Don't compress picked videos if possible
  549. if( CHECK_IOS_VERSION( @"11.0" ) )
  550. imagePicker.videoExportPreset = AVAssetExportPresetPassthrough;
  551. }
  552. #endif
  553. UIViewController *rootViewController = UnityGetGLViewController();
  554. if( UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone ) // iPhone
  555. [rootViewController presentViewController:imagePicker animated:YES completion:^{ imagePickerState = 0; }];
  556. else
  557. {
  558. // iPad
  559. popup = [[UIPopoverController alloc] initWithContentViewController:imagePicker];
  560. popup.delegate = (id) self;
  561. [popup presentPopoverFromRect:CGRectMake( rootViewController.view.frame.size.width / 2, rootViewController.view.frame.size.height / 2, 1, 1 ) inView:rootViewController.view permittedArrowDirections:0 animated:YES];
  562. }
  563. }
  564. }
  565. + (int)isMediaPickerBusy
  566. {
  567. if( imagePickerState == 2 )
  568. return 1;
  569. if( imagePicker != nil )
  570. {
  571. if( imagePickerState == 1 || [imagePicker presentingViewController] == UnityGetGLViewController() )
  572. return 1;
  573. else
  574. {
  575. imagePicker = nil;
  576. return 0;
  577. }
  578. }
  579. #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000
  580. else if( CHECK_IOS_VERSION( @"14.0" ) && imagePickerNew != nil )
  581. {
  582. if( imagePickerState == 1 || [imagePickerNew presentingViewController] == UnityGetGLViewController() )
  583. return 1;
  584. else
  585. {
  586. imagePickerNew = nil;
  587. return 0;
  588. }
  589. }
  590. #endif
  591. else
  592. return 0;
  593. }
  594. #pragma clang diagnostic push
  595. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  596. + (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
  597. {
  598. NSString *resultPath = nil;
  599. if( [info[UIImagePickerControllerMediaType] isEqualToString:(NSString *)kUTTypeImage] )
  600. {
  601. NSLog( @"Picked an image" );
  602. // On iOS 8.0 or later, try to obtain the raw data of the image (which allows picking gifs properly or preserving metadata)
  603. if( CHECK_IOS_VERSION( @"8.0" ) )
  604. {
  605. PHAsset *asset = nil;
  606. #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000
  607. if( CHECK_IOS_VERSION( @"11.0" ) )
  608. {
  609. // Try fetching the source image via UIImagePickerControllerImageURL
  610. NSURL *mediaUrl = info[UIImagePickerControllerImageURL];
  611. if( mediaUrl != nil )
  612. {
  613. NSString *imagePath = [mediaUrl path];
  614. if( imagePath != nil && [[NSFileManager defaultManager] fileExistsAtPath:imagePath] )
  615. {
  616. NSError *error;
  617. NSString *newPath = [pickedMediaSavePath stringByAppendingPathExtension:[imagePath pathExtension]];
  618. if( ![[NSFileManager defaultManager] fileExistsAtPath:newPath] || [[NSFileManager defaultManager] removeItemAtPath:newPath error:&error] )
  619. {
  620. if( [[NSFileManager defaultManager] copyItemAtPath:imagePath toPath:newPath error:&error] )
  621. {
  622. resultPath = newPath;
  623. NSLog( @"Copied source image from UIImagePickerControllerImageURL" );
  624. }
  625. else
  626. NSLog( @"Error copying image: %@", error );
  627. }
  628. else
  629. NSLog( @"Error deleting existing image: %@", error );
  630. }
  631. }
  632. if( resultPath == nil )
  633. asset = info[UIImagePickerControllerPHAsset];
  634. }
  635. #endif
  636. if( resultPath == nil && !simpleMediaPickMode )
  637. {
  638. if( asset == nil )
  639. {
  640. NSURL *mediaUrl = info[UIImagePickerControllerReferenceURL] ?: info[UIImagePickerControllerMediaURL];
  641. if( mediaUrl != nil )
  642. asset = [[PHAsset fetchAssetsWithALAssetURLs:[NSArray arrayWithObject:mediaUrl] options:nil] firstObject];
  643. }
  644. resultPath = [self trySavePHAsset:asset atIndex:1];
  645. }
  646. }
  647. if( resultPath == nil )
  648. {
  649. // Save image as PNG
  650. UIImage *image = info[UIImagePickerControllerOriginalImage];
  651. if( image != nil )
  652. {
  653. resultPath = [pickedMediaSavePath stringByAppendingPathExtension:@"png"];
  654. if( ![self saveImageAsPNG:image toPath:resultPath] )
  655. {
  656. NSLog( @"Error creating PNG image" );
  657. resultPath = nil;
  658. }
  659. }
  660. else
  661. NSLog( @"Error fetching original image from picker" );
  662. }
  663. }
  664. else if( CHECK_IOS_VERSION( @"9.1" ) && [info[UIImagePickerControllerMediaType] isEqualToString:(NSString *)kUTTypeLivePhoto] )
  665. {
  666. NSLog( @"Picked a live photo" );
  667. // Save live photo as PNG
  668. UIImage *image = info[UIImagePickerControllerOriginalImage];
  669. if( image != nil )
  670. {
  671. resultPath = [pickedMediaSavePath stringByAppendingPathExtension:@"png"];
  672. if( ![self saveImageAsPNG:image toPath:resultPath] )
  673. {
  674. NSLog( @"Error creating PNG image" );
  675. resultPath = nil;
  676. }
  677. }
  678. else
  679. NSLog( @"Error fetching live photo's still image from picker" );
  680. }
  681. else
  682. {
  683. NSLog( @"Picked a video" );
  684. NSURL *mediaUrl = info[UIImagePickerControllerMediaURL] ?: info[UIImagePickerControllerReferenceURL];
  685. if( mediaUrl != nil )
  686. {
  687. resultPath = [mediaUrl path];
  688. // On iOS 13, picked file becomes unreachable as soon as the UIImagePickerController disappears,
  689. // in that case, copy the video to a temporary location
  690. if( CHECK_IOS_VERSION( @"13.0" ) )
  691. {
  692. NSError *error;
  693. NSString *newPath = [pickedMediaSavePath stringByAppendingPathExtension:[resultPath pathExtension]];
  694. if( ![[NSFileManager defaultManager] fileExistsAtPath:newPath] || [[NSFileManager defaultManager] removeItemAtPath:newPath error:&error] )
  695. {
  696. if( [[NSFileManager defaultManager] copyItemAtPath:resultPath toPath:newPath error:&error] )
  697. resultPath = newPath;
  698. else
  699. {
  700. NSLog( @"Error copying video: %@", error );
  701. resultPath = nil;
  702. }
  703. }
  704. else
  705. {
  706. NSLog( @"Error deleting existing video: %@", error );
  707. resultPath = nil;
  708. }
  709. }
  710. }
  711. }
  712. popup = nil;
  713. imagePicker = nil;
  714. imagePickerState = 2;
  715. UnitySendMessage( "NGMediaReceiveCallbackiOS", "OnMediaReceived", [self getCString:resultPath] );
  716. [picker dismissViewControllerAnimated:NO completion:nil];
  717. }
  718. #pragma clang diagnostic pop
  719. #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000
  720. // Credit: https://ikyle.me/blog/2020/phpickerviewcontroller
  721. +(void)picker:(PHPickerViewController *)picker didFinishPicking:(NSArray<PHPickerResult *> *)results
  722. {
  723. imagePickerNew = nil;
  724. imagePickerState = 2;
  725. [picker dismissViewControllerAnimated:NO completion:nil];
  726. if( results != nil && [results count] > 0 )
  727. {
  728. NSMutableArray<NSString *> *resultPaths = [NSMutableArray arrayWithCapacity:[results count]];
  729. NSLock *arrayLock = [[NSLock alloc] init];
  730. dispatch_group_t group = dispatch_group_create();
  731. for( int i = 0; i < [results count]; i++ )
  732. {
  733. PHPickerResult *result = results[i];
  734. NSItemProvider *itemProvider = result.itemProvider;
  735. NSString *assetIdentifier = result.assetIdentifier;
  736. __block NSString *resultPath = nil;
  737. int j = i + 1;
  738. //NSLog( @"result: %@", result );
  739. //NSLog( @"%@", result.assetIdentifier);
  740. //NSLog( @"%@", result.itemProvider);
  741. if( [itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage] )
  742. {
  743. NSLog( @"Picked an image" );
  744. if( !simpleMediaPickMode && assetIdentifier != nil )
  745. {
  746. PHAsset *asset = [[PHAsset fetchAssetsWithLocalIdentifiers:[NSArray arrayWithObject:assetIdentifier] options:nil] firstObject];
  747. resultPath = [self trySavePHAsset:asset atIndex:j];
  748. }
  749. if( resultPath != nil )
  750. {
  751. [arrayLock lock];
  752. [resultPaths addObject:resultPath];
  753. [arrayLock unlock];
  754. }
  755. else
  756. {
  757. dispatch_group_enter( group );
  758. [itemProvider loadFileRepresentationForTypeIdentifier:(NSString *)kUTTypeImage completionHandler:^( NSURL *url, NSError *error )
  759. {
  760. if( url != nil )
  761. {
  762. // Copy the image to a temporary location because the returned image will be deleted by the OS after this callback is completed
  763. resultPath = [url path];
  764. NSString *newPath = [[NSString stringWithFormat:@"%@%d", pickedMediaSavePath, j] stringByAppendingPathExtension:[resultPath pathExtension]];
  765. if( ![[NSFileManager defaultManager] fileExistsAtPath:newPath] || [[NSFileManager defaultManager] removeItemAtPath:newPath error:&error] )
  766. {
  767. if( [[NSFileManager defaultManager] copyItemAtPath:resultPath toPath:newPath error:&error])
  768. resultPath = newPath;
  769. else
  770. {
  771. NSLog( @"Error copying image: %@", error );
  772. resultPath = nil;
  773. }
  774. }
  775. else
  776. {
  777. NSLog( @"Error deleting existing image: %@", error );
  778. resultPath = nil;
  779. }
  780. }
  781. else
  782. NSLog( @"Error getting the picked image's path: %@", error );
  783. if( resultPath != nil )
  784. {
  785. [arrayLock lock];
  786. [resultPaths addObject:resultPath];
  787. [arrayLock unlock];
  788. }
  789. else
  790. {
  791. if( [itemProvider canLoadObjectOfClass:[UIImage class]] )
  792. {
  793. dispatch_group_enter( group );
  794. [itemProvider loadObjectOfClass:[UIImage class] completionHandler:^( __kindof id<NSItemProviderReading> object, NSError *error )
  795. {
  796. if( object != nil && [object isKindOfClass:[UIImage class]] )
  797. {
  798. resultPath = [[NSString stringWithFormat:@"%@%d", pickedMediaSavePath, j] stringByAppendingPathExtension:@"png"];
  799. if( ![self saveImageAsPNG:(UIImage *)object toPath:resultPath] )
  800. {
  801. NSLog( @"Error creating PNG image" );
  802. resultPath = nil;
  803. }
  804. }
  805. else
  806. NSLog( @"Error generating UIImage from picked image: %@", error );
  807. [arrayLock lock];
  808. [resultPaths addObject:( resultPath != nil ? resultPath : @"" )];
  809. [arrayLock unlock];
  810. dispatch_group_leave( group );
  811. }];
  812. }
  813. else
  814. {
  815. NSLog( @"Can't generate UIImage from picked image" );
  816. [arrayLock lock];
  817. [resultPaths addObject:@""];
  818. [arrayLock unlock];
  819. }
  820. }
  821. dispatch_group_leave( group );
  822. }];
  823. }
  824. }
  825. else if( CHECK_IOS_VERSION( @"9.1" ) && [itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeLivePhoto] )
  826. {
  827. NSLog( @"Picked a live photo" );
  828. if( [itemProvider canLoadObjectOfClass:[UIImage class]] )
  829. {
  830. dispatch_group_enter( group );
  831. [itemProvider loadObjectOfClass:[UIImage class] completionHandler:^( __kindof id<NSItemProviderReading> object, NSError *error )
  832. {
  833. if( object != nil && [object isKindOfClass:[UIImage class]] )
  834. {
  835. resultPath = [[NSString stringWithFormat:@"%@%d", pickedMediaSavePath, j] stringByAppendingPathExtension:@"png"];
  836. if( ![self saveImageAsPNG:(UIImage *)object toPath:resultPath] )
  837. {
  838. NSLog( @"Error creating PNG image" );
  839. resultPath = nil;
  840. }
  841. }
  842. else
  843. NSLog( @"Error generating UIImage from picked live photo: %@", error );
  844. [arrayLock lock];
  845. [resultPaths addObject:( resultPath != nil ? resultPath : @"" )];
  846. [arrayLock unlock];
  847. dispatch_group_leave( group );
  848. }];
  849. }
  850. else if( [itemProvider canLoadObjectOfClass:[PHLivePhoto class]] )
  851. {
  852. dispatch_group_enter( group );
  853. [itemProvider loadObjectOfClass:[PHLivePhoto class] completionHandler:^( __kindof id<NSItemProviderReading> object, NSError *error )
  854. {
  855. if( object != nil && [object isKindOfClass:[PHLivePhoto class]] )
  856. {
  857. // Extract image data from live photo
  858. // Credit: https://stackoverflow.com/a/41341675/2373034
  859. NSArray<PHAssetResource*>* livePhotoResources = [PHAssetResource assetResourcesForLivePhoto:(PHLivePhoto *)object];
  860. PHAssetResource *livePhotoImage = nil;
  861. for( int k = 0; k < [livePhotoResources count]; k++ )
  862. {
  863. if( livePhotoResources[k].type == PHAssetResourceTypePhoto )
  864. {
  865. livePhotoImage = livePhotoResources[k];
  866. break;
  867. }
  868. }
  869. if( livePhotoImage == nil )
  870. {
  871. NSLog( @"Error extracting image data from live photo" );
  872. [arrayLock lock];
  873. [resultPaths addObject:@""];
  874. [arrayLock unlock];
  875. }
  876. else
  877. {
  878. dispatch_group_enter( group );
  879. NSString *originalFilename = livePhotoImage.originalFilename;
  880. if( originalFilename == nil || [originalFilename length] == 0 )
  881. resultPath = [NSString stringWithFormat:@"%@%d", pickedMediaSavePath, j];
  882. else
  883. resultPath = [[NSString stringWithFormat:@"%@%d", pickedMediaSavePath, j] stringByAppendingPathExtension:[originalFilename pathExtension]];
  884. [[PHAssetResourceManager defaultManager] writeDataForAssetResource:livePhotoImage toFile:[NSURL fileURLWithPath:resultPath] options:nil completionHandler:^( NSError * _Nullable error2 )
  885. {
  886. if( error2 != nil )
  887. {
  888. NSLog( @"Error saving image data from live photo: %@", error2 );
  889. resultPath = nil;
  890. }
  891. [arrayLock lock];
  892. [resultPaths addObject:( resultPath != nil ? resultPath : @"" )];
  893. [arrayLock unlock];
  894. dispatch_group_leave( group );
  895. }];
  896. }
  897. }
  898. else
  899. {
  900. NSLog( @"Error generating PHLivePhoto from picked live photo: %@", error );
  901. [arrayLock lock];
  902. [resultPaths addObject:@""];
  903. [arrayLock unlock];
  904. }
  905. dispatch_group_leave( group );
  906. }];
  907. }
  908. else
  909. {
  910. NSLog( @"Can't convert picked live photo to still image" );
  911. [arrayLock lock];
  912. [resultPaths addObject:@""];
  913. [arrayLock unlock];
  914. }
  915. }
  916. else if( [itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeMovie] || [itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeVideo] )
  917. {
  918. NSLog( @"Picked a video" );
  919. // Get the video file's path
  920. dispatch_group_enter( group );
  921. [itemProvider loadFileRepresentationForTypeIdentifier:([itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeMovie] ? (NSString *)kUTTypeMovie : (NSString *)kUTTypeVideo) completionHandler:^( NSURL *url, NSError *error )
  922. {
  923. if( url != nil )
  924. {
  925. // Copy the video to a temporary location because the returned video will be deleted by the OS after this callback is completed
  926. resultPath = [url path];
  927. NSString *newPath = [[NSString stringWithFormat:@"%@%d", pickedMediaSavePath, j] stringByAppendingPathExtension:[resultPath pathExtension]];
  928. if( ![[NSFileManager defaultManager] fileExistsAtPath:newPath] || [[NSFileManager defaultManager] removeItemAtPath:newPath error:&error] )
  929. {
  930. if( [[NSFileManager defaultManager] copyItemAtPath:resultPath toPath:newPath error:&error])
  931. resultPath = newPath;
  932. else
  933. {
  934. NSLog( @"Error copying video: %@", error );
  935. resultPath = nil;
  936. }
  937. }
  938. else
  939. {
  940. NSLog( @"Error deleting existing video: %@", error );
  941. resultPath = nil;
  942. }
  943. }
  944. else
  945. NSLog( @"Error getting the picked video's path: %@", error );
  946. [arrayLock lock];
  947. [resultPaths addObject:( resultPath != nil ? resultPath : @"" )];
  948. [arrayLock unlock];
  949. dispatch_group_leave( group );
  950. }];
  951. }
  952. else
  953. {
  954. // Unknown media type picked?
  955. NSLog( @"Couldn't determine type of picked media: %@", itemProvider );
  956. [arrayLock lock];
  957. [resultPaths addObject:@""];
  958. [arrayLock unlock];
  959. }
  960. }
  961. dispatch_group_notify( group, dispatch_get_main_queue(),
  962. ^{
  963. if( !pickingMultipleFiles )
  964. UnitySendMessage( "NGMediaReceiveCallbackiOS", "OnMediaReceived", [self getCString:resultPaths[0]] );
  965. else
  966. UnitySendMessage( "NGMediaReceiveCallbackiOS", "OnMultipleMediaReceived", [self getCString:[resultPaths componentsJoinedByString:@">"]] );
  967. });
  968. }
  969. else
  970. {
  971. NSLog( @"No media picked" );
  972. if( !pickingMultipleFiles )
  973. UnitySendMessage( "NGMediaReceiveCallbackiOS", "OnMediaReceived", "" );
  974. else
  975. UnitySendMessage( "NGMediaReceiveCallbackiOS", "OnMultipleMediaReceived", "" );
  976. }
  977. }
  978. #endif
  979. + (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker
  980. {
  981. NSLog( @"UIImagePickerController cancelled" );
  982. popup = nil;
  983. imagePicker = nil;
  984. UnitySendMessage( "NGMediaReceiveCallbackiOS", "OnMediaReceived", "" );
  985. [picker dismissViewControllerAnimated:NO completion:nil];
  986. }
  987. + (void)popoverControllerDidDismissPopover:(UIPopoverController *)popoverController
  988. {
  989. NSLog( @"UIPopoverController dismissed" );
  990. popup = nil;
  991. imagePicker = nil;
  992. UnitySendMessage( "NGMediaReceiveCallbackiOS", "OnMediaReceived", "" );
  993. }
  994. + (NSString *)trySavePHAsset:(PHAsset *)asset atIndex:(int)filenameIndex
  995. {
  996. if( asset == nil )
  997. return nil;
  998. __block NSString *resultPath = nil;
  999. PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init];
  1000. options.synchronous = YES;
  1001. options.version = PHImageRequestOptionsVersionCurrent;
  1002. #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
  1003. if( CHECK_IOS_VERSION( @"13.0" ) )
  1004. {
  1005. [[PHImageManager defaultManager] requestImageDataAndOrientationForAsset:asset options:options resultHandler:^( NSData *imageData, NSString *dataUTI, CGImagePropertyOrientation orientation, NSDictionary *imageInfo )
  1006. {
  1007. if( imageData != nil )
  1008. resultPath = [self trySaveSourceImage:imageData withInfo:imageInfo atIndex:filenameIndex];
  1009. else
  1010. NSLog( @"Couldn't fetch raw image data" );
  1011. }];
  1012. }
  1013. else
  1014. #endif
  1015. {
  1016. [[PHImageManager defaultManager] requestImageDataForAsset:asset options:options resultHandler:^( NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *imageInfo )
  1017. {
  1018. if( imageData != nil )
  1019. resultPath = [self trySaveSourceImage:imageData withInfo:imageInfo atIndex:filenameIndex];
  1020. else
  1021. NSLog( @"Couldn't fetch raw image data" );
  1022. }];
  1023. }
  1024. return resultPath;
  1025. }
  1026. + (NSString *)trySaveSourceImage:(NSData *)imageData withInfo:(NSDictionary *)info atIndex:(int)filenameIndex
  1027. {
  1028. NSString *filePath = info[@"PHImageFileURLKey"];
  1029. if( filePath != nil ) // filePath can actually be an NSURL, convert it to NSString
  1030. filePath = [NSString stringWithFormat:@"%@", filePath];
  1031. if( filePath == nil || [filePath length] == 0 )
  1032. {
  1033. filePath = info[@"PHImageFileUTIKey"];
  1034. if( filePath != nil )
  1035. filePath = [NSString stringWithFormat:@"%@", filePath];
  1036. }
  1037. NSString *resultPath;
  1038. if( filePath == nil || [filePath length] == 0 )
  1039. resultPath = [NSString stringWithFormat:@"%@%d", pickedMediaSavePath, filenameIndex];
  1040. else
  1041. resultPath = [[NSString stringWithFormat:@"%@%d", pickedMediaSavePath, filenameIndex] stringByAppendingPathExtension:[filePath pathExtension]];
  1042. NSError *error;
  1043. if( ![[NSFileManager defaultManager] fileExistsAtPath:resultPath] || [[NSFileManager defaultManager] removeItemAtPath:resultPath error:&error] )
  1044. {
  1045. if( ![imageData writeToFile:resultPath atomically:YES] )
  1046. {
  1047. NSLog( @"Error copying source image to file" );
  1048. resultPath = nil;
  1049. }
  1050. }
  1051. else
  1052. {
  1053. NSLog( @"Error deleting existing image: %@", error );
  1054. resultPath = nil;
  1055. }
  1056. return resultPath;
  1057. }
  1058. // Credit: https://lists.apple.com/archives/cocoa-dev/2012/Jan/msg00052.html
  1059. + (int)getMediaTypeFromExtension:(NSString *)extension
  1060. {
  1061. CFStringRef fileUTI = UTTypeCreatePreferredIdentifierForTag( kUTTagClassFilenameExtension, (__bridge CFStringRef) extension, NULL );
  1062. // mediaType is a bitmask:
  1063. // 1: image
  1064. // 2: video
  1065. // 4: audio (not supported)
  1066. int result = 0;
  1067. if( UTTypeConformsTo( fileUTI, kUTTypeImage ) )
  1068. result = 1;
  1069. else if( CHECK_IOS_VERSION( @"9.1" ) && UTTypeConformsTo( fileUTI, kUTTypeLivePhoto ) )
  1070. result = 1;
  1071. else if( UTTypeConformsTo( fileUTI, kUTTypeMovie ) || UTTypeConformsTo( fileUTI, kUTTypeVideo ) )
  1072. result = 2;
  1073. else if( UTTypeConformsTo( fileUTI, kUTTypeAudio ) )
  1074. result = 4;
  1075. CFRelease( fileUTI );
  1076. return result;
  1077. }
  1078. // Credit: https://stackoverflow.com/a/4170099/2373034
  1079. + (NSArray *)getImageMetadata:(NSString *)path
  1080. {
  1081. int width = 0;
  1082. int height = 0;
  1083. int orientation = -1;
  1084. CGImageSourceRef imageSource = CGImageSourceCreateWithURL( (__bridge CFURLRef) [NSURL fileURLWithPath:path], nil );
  1085. if( imageSource != nil )
  1086. {
  1087. NSDictionary *options = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:NO] forKey:(__bridge NSString *)kCGImageSourceShouldCache];
  1088. CFDictionaryRef imageProperties = CGImageSourceCopyPropertiesAtIndex( imageSource, 0, (__bridge CFDictionaryRef) options );
  1089. CFRelease( imageSource );
  1090. CGFloat widthF = 0.0f, heightF = 0.0f;
  1091. if( imageProperties != nil )
  1092. {
  1093. if( CFDictionaryContainsKey( imageProperties, kCGImagePropertyPixelWidth ) )
  1094. CFNumberGetValue( (CFNumberRef) CFDictionaryGetValue( imageProperties, kCGImagePropertyPixelWidth ), kCFNumberCGFloatType, &widthF );
  1095. if( CFDictionaryContainsKey( imageProperties, kCGImagePropertyPixelHeight ) )
  1096. CFNumberGetValue( (CFNumberRef) CFDictionaryGetValue( imageProperties, kCGImagePropertyPixelHeight ), kCFNumberCGFloatType, &heightF );
  1097. if( CFDictionaryContainsKey( imageProperties, kCGImagePropertyOrientation ) )
  1098. {
  1099. CFNumberGetValue( (CFNumberRef) CFDictionaryGetValue( imageProperties, kCGImagePropertyOrientation ), kCFNumberIntType, &orientation );
  1100. if( orientation > 4 )
  1101. {
  1102. // Landscape image
  1103. CGFloat temp = widthF;
  1104. widthF = heightF;
  1105. heightF = temp;
  1106. }
  1107. }
  1108. CFRelease( imageProperties );
  1109. }
  1110. width = (int) roundf( widthF );
  1111. height = (int) roundf( heightF );
  1112. }
  1113. return [[NSArray alloc] initWithObjects:[NSNumber numberWithInt:width], [NSNumber numberWithInt:height], [NSNumber numberWithInt:orientation], nil];
  1114. }
  1115. + (char *)getImageProperties:(NSString *)path
  1116. {
  1117. NSArray *metadata = [self getImageMetadata:path];
  1118. int orientationUnity;
  1119. int orientation = [metadata[2] intValue];
  1120. // To understand the magic numbers, see ImageOrientation enum in NativeGallery.cs
  1121. // and http://sylvana.net/jpegcrop/exif_orientation.html
  1122. if( orientation == 1 )
  1123. orientationUnity = 0;
  1124. else if( orientation == 2 )
  1125. orientationUnity = 4;
  1126. else if( orientation == 3 )
  1127. orientationUnity = 2;
  1128. else if( orientation == 4 )
  1129. orientationUnity = 6;
  1130. else if( orientation == 5 )
  1131. orientationUnity = 5;
  1132. else if( orientation == 6 )
  1133. orientationUnity = 1;
  1134. else if( orientation == 7 )
  1135. orientationUnity = 7;
  1136. else if( orientation == 8 )
  1137. orientationUnity = 3;
  1138. else
  1139. orientationUnity = -1;
  1140. return [self getCString:[NSString stringWithFormat:@"%d>%d> >%d", [metadata[0] intValue], [metadata[1] intValue], orientationUnity]];
  1141. }
  1142. + (char *)getVideoProperties:(NSString *)path
  1143. {
  1144. CGSize size = CGSizeZero;
  1145. float rotation = 0;
  1146. long long duration = 0;
  1147. AVURLAsset *asset = [AVURLAsset URLAssetWithURL:[NSURL fileURLWithPath:path] options:nil];
  1148. if( asset != nil )
  1149. {
  1150. duration = (long long) round( CMTimeGetSeconds( [asset duration] ) * 1000 );
  1151. CGAffineTransform transform = [asset preferredTransform];
  1152. NSArray<AVAssetTrack *>* videoTracks = [asset tracksWithMediaType:AVMediaTypeVideo];
  1153. if( videoTracks != nil && [videoTracks count] > 0 )
  1154. {
  1155. size = [[videoTracks objectAtIndex:0] naturalSize];
  1156. transform = [[videoTracks objectAtIndex:0] preferredTransform];
  1157. }
  1158. rotation = atan2( transform.b, transform.a ) * ( 180.0 / M_PI );
  1159. }
  1160. return [self getCString:[NSString stringWithFormat:@"%d>%d>%lld>%f", (int) roundf( size.width ), (int) roundf( size.height ), duration, rotation]];
  1161. }
  1162. + (char *)getVideoThumbnail:(NSString *)path savePath:(NSString *)savePath maximumSize:(int)maximumSize captureTime:(double)captureTime
  1163. {
  1164. AVAssetImageGenerator *thumbnailGenerator = [[AVAssetImageGenerator alloc] initWithAsset:[[AVURLAsset alloc] initWithURL:[NSURL fileURLWithPath:path] options:nil]];
  1165. thumbnailGenerator.appliesPreferredTrackTransform = YES;
  1166. thumbnailGenerator.maximumSize = CGSizeMake( (CGFloat) maximumSize, (CGFloat) maximumSize );
  1167. thumbnailGenerator.requestedTimeToleranceBefore = kCMTimeZero;
  1168. thumbnailGenerator.requestedTimeToleranceAfter = kCMTimeZero;
  1169. if( captureTime < 0.0 )
  1170. captureTime = 0.0;
  1171. else
  1172. {
  1173. AVURLAsset *asset = [AVURLAsset URLAssetWithURL:[NSURL fileURLWithPath:path] options:nil];
  1174. if( asset != nil )
  1175. {
  1176. double videoDuration = CMTimeGetSeconds( [asset duration] );
  1177. if( videoDuration > 0.0 && captureTime >= videoDuration - 0.1 )
  1178. {
  1179. if( captureTime > videoDuration )
  1180. captureTime = videoDuration;
  1181. thumbnailGenerator.requestedTimeToleranceBefore = CMTimeMakeWithSeconds( 1.0, 600 );
  1182. }
  1183. }
  1184. }
  1185. NSError *error = nil;
  1186. CGImageRef image = [thumbnailGenerator copyCGImageAtTime:CMTimeMakeWithSeconds( captureTime, 600 ) actualTime:nil error:&error];
  1187. if( image == nil )
  1188. {
  1189. if( error != nil )
  1190. NSLog( @"Error generating video thumbnail: %@", error );
  1191. else
  1192. NSLog( @"Error generating video thumbnail..." );
  1193. return [self getCString:@""];
  1194. }
  1195. UIImage *thumbnail = [[UIImage alloc] initWithCGImage:image];
  1196. CGImageRelease( image );
  1197. if( ![UIImagePNGRepresentation( thumbnail ) writeToFile:savePath atomically:YES] )
  1198. {
  1199. NSLog( @"Error saving thumbnail image" );
  1200. return [self getCString:@""];
  1201. }
  1202. return [self getCString:savePath];
  1203. }
  1204. + (BOOL)saveImageAsPNG:(UIImage *)image toPath:(NSString *)resultPath
  1205. {
  1206. return [UIImagePNGRepresentation( [self scaleImage:image maxSize:16384] ) writeToFile:resultPath atomically:YES];
  1207. }
  1208. + (UIImage *)scaleImage:(UIImage *)image maxSize:(int)maxSize
  1209. {
  1210. CGFloat width = image.size.width;
  1211. CGFloat height = image.size.height;
  1212. UIImageOrientation orientation = image.imageOrientation;
  1213. if( width <= maxSize && height <= maxSize && orientation != UIImageOrientationDown &&
  1214. orientation != UIImageOrientationLeft && orientation != UIImageOrientationRight &&
  1215. orientation != UIImageOrientationLeftMirrored && orientation != UIImageOrientationRightMirrored &&
  1216. orientation != UIImageOrientationUpMirrored && orientation != UIImageOrientationDownMirrored )
  1217. return image;
  1218. CGFloat scaleX = 1.0f;
  1219. CGFloat scaleY = 1.0f;
  1220. if( width > maxSize )
  1221. scaleX = maxSize / width;
  1222. if( height > maxSize )
  1223. scaleY = maxSize / height;
  1224. // Credit: https://github.com/mbcharbonneau/UIImage-Categories/blob/master/UIImage%2BAlpha.m
  1225. CGImageAlphaInfo alpha = CGImageGetAlphaInfo( image.CGImage );
  1226. BOOL hasAlpha = alpha == kCGImageAlphaFirst || alpha == kCGImageAlphaLast || alpha == kCGImageAlphaPremultipliedFirst || alpha == kCGImageAlphaPremultipliedLast;
  1227. CGFloat scaleRatio = scaleX < scaleY ? scaleX : scaleY;
  1228. CGRect imageRect = CGRectMake( 0, 0, width * scaleRatio, height * scaleRatio );
  1229. #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 100000
  1230. // Resize image with UIGraphicsImageRenderer (Apple's recommended API) if possible
  1231. if( CHECK_IOS_VERSION( @"10.0" ) )
  1232. {
  1233. UIGraphicsImageRendererFormat *format = [image imageRendererFormat];
  1234. format.opaque = !hasAlpha;
  1235. format.scale = image.scale;
  1236. UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:imageRect.size format:format];
  1237. image = [renderer imageWithActions:^( UIGraphicsImageRendererContext* _Nonnull myContext )
  1238. {
  1239. [image drawInRect:imageRect];
  1240. }];
  1241. }
  1242. else
  1243. #endif
  1244. {
  1245. UIGraphicsBeginImageContextWithOptions( imageRect.size, !hasAlpha, image.scale );
  1246. [image drawInRect:imageRect];
  1247. image = UIGraphicsGetImageFromCurrentImageContext();
  1248. UIGraphicsEndImageContext();
  1249. }
  1250. return image;
  1251. }
  1252. + (char *)loadImageAtPath:(NSString *)path tempFilePath:(NSString *)tempFilePath maximumSize:(int)maximumSize
  1253. {
  1254. // Check if the image can be loaded by Unity without requiring a conversion to PNG
  1255. // Credit: https://stackoverflow.com/a/12048937/2373034
  1256. NSString *extension = [path pathExtension];
  1257. BOOL conversionNeeded = [extension caseInsensitiveCompare:@"jpg"] != NSOrderedSame && [extension caseInsensitiveCompare:@"jpeg"] != NSOrderedSame && [extension caseInsensitiveCompare:@"png"] != NSOrderedSame;
  1258. if( !conversionNeeded )
  1259. {
  1260. // Check if the image needs to be processed at all
  1261. NSArray *metadata = [self getImageMetadata:path];
  1262. int orientationInt = [metadata[2] intValue]; // 1: correct orientation, [1,8]: valid orientation range
  1263. if( orientationInt == 1 && [metadata[0] intValue] <= maximumSize && [metadata[1] intValue] <= maximumSize )
  1264. return [self getCString:path];
  1265. }
  1266. UIImage *image = [UIImage imageWithContentsOfFile:path];
  1267. if( image == nil )
  1268. return [self getCString:path];
  1269. UIImage *scaledImage = [self scaleImage:image maxSize:maximumSize];
  1270. if( conversionNeeded || scaledImage != image )
  1271. {
  1272. if( ![UIImagePNGRepresentation( scaledImage ) writeToFile:tempFilePath atomically:YES] )
  1273. {
  1274. NSLog( @"Error creating scaled image" );
  1275. return [self getCString:path];
  1276. }
  1277. return [self getCString:tempFilePath];
  1278. }
  1279. else
  1280. return [self getCString:path];
  1281. }
  1282. // Credit: https://stackoverflow.com/a/37052118/2373034
  1283. + (char *)getCString:(NSString *)source
  1284. {
  1285. if( source == nil )
  1286. source = @"";
  1287. const char *sourceUTF8 = [source UTF8String];
  1288. char *result = (char*) malloc( strlen( sourceUTF8 ) + 1 );
  1289. strcpy( result, sourceUTF8 );
  1290. return result;
  1291. }
  1292. @end
  1293. extern "C" int _NativeGallery_CheckPermission( int readPermission, int permissionFreeMode )
  1294. {
  1295. return [UNativeGallery checkPermission:( readPermission == 1 ) permissionFreeMode:( permissionFreeMode == 1 )];
  1296. }
  1297. extern "C" int _NativeGallery_RequestPermission( int readPermission, int permissionFreeMode )
  1298. {
  1299. return [UNativeGallery requestPermission:( readPermission == 1 ) permissionFreeMode:( permissionFreeMode == 1 )];
  1300. }
  1301. extern "C" void _NativeGallery_ShowLimitedLibraryPicker()
  1302. {
  1303. return [UNativeGallery showLimitedLibraryPicker];
  1304. }
  1305. extern "C" int _NativeGallery_CanOpenSettings()
  1306. {
  1307. return [UNativeGallery canOpenSettings];
  1308. }
  1309. extern "C" void _NativeGallery_OpenSettings()
  1310. {
  1311. [UNativeGallery openSettings];
  1312. }
  1313. extern "C" int _NativeGallery_CanPickMultipleMedia()
  1314. {
  1315. return [UNativeGallery canPickMultipleMedia];
  1316. }
  1317. extern "C" void _NativeGallery_ImageWriteToAlbum( const char* path, const char* album, int permissionFreeMode )
  1318. {
  1319. [UNativeGallery saveMedia:[NSString stringWithUTF8String:path] albumName:[NSString stringWithUTF8String:album] isImg:YES permissionFreeMode:( permissionFreeMode == 1 )];
  1320. }
  1321. extern "C" void _NativeGallery_VideoWriteToAlbum( const char* path, const char* album, int permissionFreeMode )
  1322. {
  1323. [UNativeGallery saveMedia:[NSString stringWithUTF8String:path] albumName:[NSString stringWithUTF8String:album] isImg:NO permissionFreeMode:( permissionFreeMode == 1 )];
  1324. }
  1325. extern "C" void _NativeGallery_PickMedia( const char* mediaSavePath, int mediaType, int permissionFreeMode, int selectionLimit )
  1326. {
  1327. [UNativeGallery pickMedia:mediaType savePath:[NSString stringWithUTF8String:mediaSavePath] permissionFreeMode:( permissionFreeMode == 1 ) selectionLimit:selectionLimit];
  1328. }
  1329. extern "C" int _NativeGallery_IsMediaPickerBusy()
  1330. {
  1331. return [UNativeGallery isMediaPickerBusy];
  1332. }
  1333. extern "C" int _NativeGallery_GetMediaTypeFromExtension( const char* extension )
  1334. {
  1335. return [UNativeGallery getMediaTypeFromExtension:[NSString stringWithUTF8String:extension]];
  1336. }
  1337. extern "C" char* _NativeGallery_GetImageProperties( const char* path )
  1338. {
  1339. return [UNativeGallery getImageProperties:[NSString stringWithUTF8String:path]];
  1340. }
  1341. extern "C" char* _NativeGallery_GetVideoProperties( const char* path )
  1342. {
  1343. return [UNativeGallery getVideoProperties:[NSString stringWithUTF8String:path]];
  1344. }
  1345. extern "C" char* _NativeGallery_GetVideoThumbnail( const char* path, const char* thumbnailSavePath, int maxSize, double captureTimeInSeconds )
  1346. {
  1347. return [UNativeGallery getVideoThumbnail:[NSString stringWithUTF8String:path] savePath:[NSString stringWithUTF8String:thumbnailSavePath] maximumSize:maxSize captureTime:captureTimeInSeconds];
  1348. }
  1349. extern "C" char* _NativeGallery_LoadImageAtPath( const char* path, const char* temporaryFilePath, int maxSize )
  1350. {
  1351. return [UNativeGallery loadImageAtPath:[NSString stringWithUTF8String:path] tempFilePath:[NSString stringWithUTF8String:temporaryFilePath] maximumSize:maxSize];
  1352. }