diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery.meta b/TheStrongestSnail/Assets/Plugins/NativeGallery.meta new file mode 100644 index 0000000..0c80a47 --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 5e05ed2bddbccb94e9650efb5742e452 +folderAsset: yes +timeCreated: 1518877529 +licenseType: Store +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/Android.meta b/TheStrongestSnail/Assets/Plugins/NativeGallery/Android.meta new file mode 100644 index 0000000..ae029ba --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/Android.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 0a607dcda26e7614f86300c6ca717295 +folderAsset: yes +timeCreated: 1498722617 +licenseType: Store +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGCallbackHelper.cs b/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGCallbackHelper.cs new file mode 100644 index 0000000..bf79515 --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGCallbackHelper.cs @@ -0,0 +1,38 @@ +#if UNITY_EDITOR || UNITY_ANDROID +using UnityEngine; + +namespace NativeGalleryNamespace +{ + public class NGCallbackHelper : MonoBehaviour + { + private System.Action mainThreadAction = null; + + private void Awake() + { + DontDestroyOnLoad( gameObject ); + } + + private void Update() + { + if( mainThreadAction != null ) + { + try + { + System.Action temp = mainThreadAction; + mainThreadAction = null; + temp(); + } + finally + { + Destroy( gameObject ); + } + } + } + + public void CallOnMainThread( System.Action function ) + { + mainThreadAction = function; + } + } +} +#endif \ No newline at end of file diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGCallbackHelper.cs.meta b/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGCallbackHelper.cs.meta new file mode 100644 index 0000000..d9323e8 --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGCallbackHelper.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 2d517fd0f2f85f24698df2775bee58e9 +timeCreated: 1544889149 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGMediaReceiveCallbackAndroid.cs b/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGMediaReceiveCallbackAndroid.cs new file mode 100644 index 0000000..f38c628 --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGMediaReceiveCallbackAndroid.cs @@ -0,0 +1,62 @@ +#if UNITY_EDITOR || UNITY_ANDROID +using UnityEngine; + +namespace NativeGalleryNamespace +{ + public class NGMediaReceiveCallbackAndroid : AndroidJavaProxy + { + private readonly NativeGallery.MediaPickCallback callback; + private readonly NativeGallery.MediaPickMultipleCallback callbackMultiple; + + private readonly NGCallbackHelper callbackHelper; + + public NGMediaReceiveCallbackAndroid( NativeGallery.MediaPickCallback callback, NativeGallery.MediaPickMultipleCallback callbackMultiple ) : base( "com.yasirkula.unity.NativeGalleryMediaReceiver" ) + { + this.callback = callback; + this.callbackMultiple = callbackMultiple; + callbackHelper = new GameObject( "NGCallbackHelper" ).AddComponent(); + } + + [UnityEngine.Scripting.Preserve] + public void OnMediaReceived( string path ) + { + callbackHelper.CallOnMainThread( () => callback( !string.IsNullOrEmpty( path ) ? path : null ) ); + } + + [UnityEngine.Scripting.Preserve] + public void OnMultipleMediaReceived( string paths ) + { + string[] result = null; + if( !string.IsNullOrEmpty( paths ) ) + { + string[] pathsSplit = paths.Split( '>' ); + + int validPathCount = 0; + for( int i = 0; i < pathsSplit.Length; i++ ) + { + if( !string.IsNullOrEmpty( pathsSplit[i] ) ) + validPathCount++; + } + + if( validPathCount == 0 ) + pathsSplit = new string[0]; + else if( validPathCount != pathsSplit.Length ) + { + string[] validPaths = new string[validPathCount]; + for( int i = 0, j = 0; i < pathsSplit.Length; i++ ) + { + if( !string.IsNullOrEmpty( pathsSplit[i] ) ) + validPaths[j++] = pathsSplit[i]; + } + + pathsSplit = validPaths; + } + + result = pathsSplit; + } + + callbackHelper.CallOnMainThread( () => callbackMultiple( ( result != null && result.Length > 0 ) ? result : null ) ); + } + } +} +#endif \ No newline at end of file diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGMediaReceiveCallbackAndroid.cs.meta b/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGMediaReceiveCallbackAndroid.cs.meta new file mode 100644 index 0000000..ab75fb0 --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGMediaReceiveCallbackAndroid.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 4c18d702b07a63945968db47201b95c9 +timeCreated: 1519060539 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGPermissionCallbackAndroid.cs b/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGPermissionCallbackAndroid.cs new file mode 100644 index 0000000..71d6b41 --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGPermissionCallbackAndroid.cs @@ -0,0 +1,48 @@ +#if UNITY_EDITOR || UNITY_ANDROID +using System.Threading; +using UnityEngine; + +namespace NativeGalleryNamespace +{ + public class NGPermissionCallbackAndroid : AndroidJavaProxy + { + private object threadLock; + public int Result { get; private set; } + + public NGPermissionCallbackAndroid( object threadLock ) : base( "com.yasirkula.unity.NativeGalleryPermissionReceiver" ) + { + Result = -1; + this.threadLock = threadLock; + } + + [UnityEngine.Scripting.Preserve] + public void OnPermissionResult( int result ) + { + Result = result; + + lock( threadLock ) + { + Monitor.Pulse( threadLock ); + } + } + } + + public class NGPermissionCallbackAsyncAndroid : AndroidJavaProxy + { + private readonly NativeGallery.PermissionCallback callback; + private readonly NGCallbackHelper callbackHelper; + + public NGPermissionCallbackAsyncAndroid( NativeGallery.PermissionCallback callback ) : base( "com.yasirkula.unity.NativeGalleryPermissionReceiver" ) + { + this.callback = callback; + callbackHelper = new GameObject( "NGCallbackHelper" ).AddComponent(); + } + + [UnityEngine.Scripting.Preserve] + public void OnPermissionResult( int result ) + { + callbackHelper.CallOnMainThread( () => callback( (NativeGallery.Permission) result ) ); + } + } +} +#endif \ No newline at end of file diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGPermissionCallbackAndroid.cs.meta b/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGPermissionCallbackAndroid.cs.meta new file mode 100644 index 0000000..1030058 --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGPermissionCallbackAndroid.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: a07afac614af1294d8e72a3c083be028 +timeCreated: 1519060539 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NativeGallery.aar b/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NativeGallery.aar new file mode 100644 index 0000000..3cac09b Binary files /dev/null and b/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NativeGallery.aar differ diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NativeGallery.aar.meta b/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NativeGallery.aar.meta new file mode 100644 index 0000000..fba9ba0 --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NativeGallery.aar.meta @@ -0,0 +1,33 @@ +fileFormatVersion: 2 +guid: db4d55e1212537e4baa84cac66eb6645 +timeCreated: 1569764737 +licenseType: Store +PluginImporter: + serializedVersion: 2 + iconMap: {} + executionOrder: {} + isPreloaded: 0 + isOverridable: 0 + platformData: + data: + first: + Android: Android + second: + enabled: 1 + settings: {} + data: + first: + Any: + second: + enabled: 0 + settings: {} + data: + first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + userData: + assetBundleName: + assetBundleVariant: diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/Editor.meta b/TheStrongestSnail/Assets/Plugins/NativeGallery/Editor.meta new file mode 100644 index 0000000..0f26bfe --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/Editor.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 19fc6b8ce781591438a952d8aa9104f8 +folderAsset: yes +timeCreated: 1521452097 +licenseType: Store +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/Editor/NGPostProcessBuild.cs b/TheStrongestSnail/Assets/Plugins/NativeGallery/Editor/NGPostProcessBuild.cs new file mode 100644 index 0000000..74cbb2d --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/Editor/NGPostProcessBuild.cs @@ -0,0 +1,152 @@ +using System.IO; +using UnityEditor; +using UnityEngine; +#if UNITY_IOS +using UnityEditor.Callbacks; +using UnityEditor.iOS.Xcode; +#endif + +namespace NativeGalleryNamespace +{ + [System.Serializable] + public class Settings + { + private const string SAVE_PATH = "ProjectSettings/NativeGallery.json"; + + public bool AutomatedSetup = true; +#if !UNITY_2018_1_OR_NEWER + public bool MinimumiOSTarget8OrAbove = false; +#endif + public string PhotoLibraryUsageDescription = "The app requires access to Photos to interact with it."; + public string PhotoLibraryAdditionsUsageDescription = "The app requires access to Photos to save media to it."; + public bool DontAskLimitedPhotosPermissionAutomaticallyOnIos14 = true; // See: https://mackuba.eu/2020/07/07/photo-library-changes-ios-14/ + + private static Settings m_instance = null; + public static Settings Instance + { + get + { + if( m_instance == null ) + { + try + { + if( File.Exists( SAVE_PATH ) ) + m_instance = JsonUtility.FromJson( File.ReadAllText( SAVE_PATH ) ); + else + m_instance = new Settings(); + } + catch( System.Exception e ) + { + Debug.LogException( e ); + m_instance = new Settings(); + } + } + + return m_instance; + } + } + + public void Save() + { + File.WriteAllText( SAVE_PATH, JsonUtility.ToJson( this, true ) ); + } + +#if UNITY_2018_3_OR_NEWER + [SettingsProvider] + public static SettingsProvider CreatePreferencesGUI() + { + return new SettingsProvider( "Project/yasirkula/Native Gallery", SettingsScope.Project ) + { + guiHandler = ( searchContext ) => PreferencesGUI(), + keywords = new System.Collections.Generic.HashSet() { "Native", "Gallery", "Android", "iOS" } + }; + } +#endif + +#if !UNITY_2018_3_OR_NEWER + [PreferenceItem( "Native Gallery" )] +#endif + public static void PreferencesGUI() + { + EditorGUI.BeginChangeCheck(); + + Instance.AutomatedSetup = EditorGUILayout.Toggle( "Automated Setup", Instance.AutomatedSetup ); + + EditorGUI.BeginDisabledGroup( !Instance.AutomatedSetup ); +#if !UNITY_2018_1_OR_NEWER + Instance.MinimumiOSTarget8OrAbove = EditorGUILayout.Toggle( "Deployment Target Is 8.0 Or Above", Instance.MinimumiOSTarget8OrAbove ); +#endif + Instance.PhotoLibraryUsageDescription = EditorGUILayout.DelayedTextField( "Photo Library Usage Description", Instance.PhotoLibraryUsageDescription ); + Instance.PhotoLibraryAdditionsUsageDescription = EditorGUILayout.DelayedTextField( "Photo Library Additions Usage Description", Instance.PhotoLibraryAdditionsUsageDescription ); + Instance.DontAskLimitedPhotosPermissionAutomaticallyOnIos14 = EditorGUILayout.Toggle( new GUIContent( "Don't Ask Limited Photos Permission Automatically", "See: https://mackuba.eu/2020/07/07/photo-library-changes-ios-14/. It's recommended to keep this setting enabled" ), Instance.DontAskLimitedPhotosPermissionAutomaticallyOnIos14 ); + EditorGUI.EndDisabledGroup(); + + if( EditorGUI.EndChangeCheck() ) + Instance.Save(); + } + } + + public class NGPostProcessBuild + { +#if UNITY_IOS + [PostProcessBuild( 1 )] + public static void OnPostprocessBuild( BuildTarget target, string buildPath ) + { + if( !Settings.Instance.AutomatedSetup ) + return; + + if( target == BuildTarget.iOS ) + { + string pbxProjectPath = PBXProject.GetPBXProjectPath( buildPath ); + string plistPath = Path.Combine( buildPath, "Info.plist" ); + + PBXProject pbxProject = new PBXProject(); + pbxProject.ReadFromFile( pbxProjectPath ); + +#if UNITY_2019_3_OR_NEWER + string targetGUID = pbxProject.GetUnityFrameworkTargetGuid(); +#else + string targetGUID = pbxProject.TargetGuidByName( PBXProject.GetUnityTargetName() ); +#endif + + // Minimum supported iOS version on Unity 2018.1 and later is 8.0 +#if !UNITY_2018_1_OR_NEWER + if( !Settings.Instance.MinimumiOSTarget8OrAbove ) + { + pbxProject.AddBuildProperty( targetGUID, "OTHER_LDFLAGS", "-weak_framework Photos" ); + pbxProject.AddBuildProperty( targetGUID, "OTHER_LDFLAGS", "-weak_framework PhotosUI" ); + pbxProject.AddBuildProperty( targetGUID, "OTHER_LDFLAGS", "-framework AssetsLibrary" ); + pbxProject.AddBuildProperty( targetGUID, "OTHER_LDFLAGS", "-framework MobileCoreServices" ); + pbxProject.AddBuildProperty( targetGUID, "OTHER_LDFLAGS", "-framework ImageIO" ); + } + else +#endif + { + pbxProject.AddBuildProperty( targetGUID, "OTHER_LDFLAGS", "-weak_framework PhotosUI" ); + pbxProject.AddBuildProperty( targetGUID, "OTHER_LDFLAGS", "-framework Photos" ); + pbxProject.AddBuildProperty( targetGUID, "OTHER_LDFLAGS", "-framework MobileCoreServices" ); + pbxProject.AddBuildProperty( targetGUID, "OTHER_LDFLAGS", "-framework ImageIO" ); + } + + pbxProject.RemoveFrameworkFromProject( targetGUID, "Photos.framework" ); + pbxProject.RemoveFrameworkFromProject( targetGUID, "PhotosUI.framework" ); + + File.WriteAllText( pbxProjectPath, pbxProject.WriteToString() ); + + PlistDocument plist = new PlistDocument(); + plist.ReadFromString( File.ReadAllText( plistPath ) ); + + PlistElementDict rootDict = plist.root; + if( !string.IsNullOrEmpty( Settings.Instance.PhotoLibraryUsageDescription ) ) + rootDict.SetString( "NSPhotoLibraryUsageDescription", Settings.Instance.PhotoLibraryUsageDescription ); + if( !string.IsNullOrEmpty( Settings.Instance.PhotoLibraryAdditionsUsageDescription ) ) + rootDict.SetString( "NSPhotoLibraryAddUsageDescription", Settings.Instance.PhotoLibraryAdditionsUsageDescription ); + if( Settings.Instance.DontAskLimitedPhotosPermissionAutomaticallyOnIos14 ) + rootDict.SetBoolean( "PHPhotoLibraryPreventAutomaticLimitedAccessAlert", true ); + + File.WriteAllText( plistPath, plist.WriteToString() ); + } + } +#endif + } +} \ No newline at end of file diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/Editor/NGPostProcessBuild.cs.meta b/TheStrongestSnail/Assets/Plugins/NativeGallery/Editor/NGPostProcessBuild.cs.meta new file mode 100644 index 0000000..bd4beff --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/Editor/NGPostProcessBuild.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: dff1540cf22bfb749a2422f445cf9427 +timeCreated: 1521452119 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/Editor/NativeGallery.Editor.asmdef b/TheStrongestSnail/Assets/Plugins/NativeGallery/Editor/NativeGallery.Editor.asmdef new file mode 100644 index 0000000..d129dd2 --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/Editor/NativeGallery.Editor.asmdef @@ -0,0 +1,15 @@ +{ + "name": "NativeGallery.Editor", + "references": [], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/Editor/NativeGallery.Editor.asmdef.meta b/TheStrongestSnail/Assets/Plugins/NativeGallery/Editor/NativeGallery.Editor.asmdef.meta new file mode 100644 index 0000000..4683509 --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/Editor/NativeGallery.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 3dffc8e654f00c545a82d0a5274d51eb +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/NativeGallery.Runtime.asmdef b/TheStrongestSnail/Assets/Plugins/NativeGallery/NativeGallery.Runtime.asmdef new file mode 100644 index 0000000..c1fb4a7 --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/NativeGallery.Runtime.asmdef @@ -0,0 +1,3 @@ +{ + "name": "NativeGallery.Runtime" +} diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/NativeGallery.Runtime.asmdef.meta b/TheStrongestSnail/Assets/Plugins/NativeGallery/NativeGallery.Runtime.asmdef.meta new file mode 100644 index 0000000..097882b --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/NativeGallery.Runtime.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 6e5063adab271564ba0098a06a8cebda +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/NativeGallery.cs b/TheStrongestSnail/Assets/Plugins/NativeGallery/NativeGallery.cs new file mode 100644 index 0000000..e080f97 --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/NativeGallery.cs @@ -0,0 +1,1039 @@ +using System; +using System.Globalization; +using System.IO; +using UnityEngine; +#if UNITY_2018_4_OR_NEWER && !NATIVE_GALLERY_DISABLE_ASYNC_FUNCTIONS +using System.Threading.Tasks; +using Unity.Collections; +using UnityEngine.Networking; +#endif +#if UNITY_ANDROID || UNITY_IOS +using NativeGalleryNamespace; +#endif +using Object = UnityEngine.Object; + +public static class NativeGallery +{ + public struct ImageProperties + { + public readonly int width; + public readonly int height; + public readonly string mimeType; + public readonly ImageOrientation orientation; + + public ImageProperties( int width, int height, string mimeType, ImageOrientation orientation ) + { + this.width = width; + this.height = height; + this.mimeType = mimeType; + this.orientation = orientation; + } + } + + public struct VideoProperties + { + public readonly int width; + public readonly int height; + public readonly long duration; + public readonly float rotation; + + public VideoProperties( int width, int height, long duration, float rotation ) + { + this.width = width; + this.height = height; + this.duration = duration; + this.rotation = rotation; + } + } + + public enum PermissionType { Read = 0, Write = 1 }; + public enum Permission { Denied = 0, Granted = 1, ShouldAsk = 2 }; + + [Flags] + public enum MediaType { Image = 1, Video = 2, Audio = 4 }; + + // EXIF orientation: http://sylvana.net/jpegcrop/exif_orientation.html (indices are reordered) + public enum ImageOrientation { Unknown = -1, Normal = 0, Rotate90 = 1, Rotate180 = 2, Rotate270 = 3, FlipHorizontal = 4, Transpose = 5, FlipVertical = 6, Transverse = 7 }; + + public delegate void PermissionCallback( Permission permission ); + public delegate void MediaSaveCallback( bool success, string path ); + public delegate void MediaPickCallback( string path ); + public delegate void MediaPickMultipleCallback( string[] paths ); + + #region Platform Specific Elements +#if !UNITY_EDITOR && UNITY_ANDROID + private static AndroidJavaClass m_ajc = null; + private static AndroidJavaClass AJC + { + get + { + if( m_ajc == null ) + m_ajc = new AndroidJavaClass( "com.yasirkula.unity.NativeGallery" ); + + return m_ajc; + } + } + + private static AndroidJavaObject m_context = null; + private static AndroidJavaObject Context + { + get + { + if( m_context == null ) + { + using( AndroidJavaObject unityClass = new AndroidJavaClass( "com.unity3d.player.UnityPlayer" ) ) + { + m_context = unityClass.GetStatic( "currentActivity" ); + } + } + + return m_context; + } + } +#elif !UNITY_EDITOR && UNITY_IOS + [System.Runtime.InteropServices.DllImport( "__Internal" )] + private static extern int _NativeGallery_CheckPermission( int readPermission, int permissionFreeMode ); + + [System.Runtime.InteropServices.DllImport( "__Internal" )] + private static extern int _NativeGallery_RequestPermission( int readPermission, int permissionFreeMode, int asyncMode ); + + [System.Runtime.InteropServices.DllImport( "__Internal" )] + private static extern void _NativeGallery_ShowLimitedLibraryPicker(); + + [System.Runtime.InteropServices.DllImport( "__Internal" )] + private static extern int _NativeGallery_CanOpenSettings(); + + [System.Runtime.InteropServices.DllImport( "__Internal" )] + private static extern void _NativeGallery_OpenSettings(); + + [System.Runtime.InteropServices.DllImport( "__Internal" )] + private static extern int _NativeGallery_CanPickMultipleMedia(); + + [System.Runtime.InteropServices.DllImport( "__Internal" )] + private static extern int _NativeGallery_GetMediaTypeFromExtension( string extension ); + + [System.Runtime.InteropServices.DllImport( "__Internal" )] + private static extern void _NativeGallery_ImageWriteToAlbum( string path, string album, int permissionFreeMode ); + + [System.Runtime.InteropServices.DllImport( "__Internal" )] + private static extern void _NativeGallery_VideoWriteToAlbum( string path, string album, int permissionFreeMode ); + + [System.Runtime.InteropServices.DllImport( "__Internal" )] + private static extern void _NativeGallery_PickMedia( string mediaSavePath, int mediaType, int permissionFreeMode, int selectionLimit ); + + [System.Runtime.InteropServices.DllImport( "__Internal" )] + private static extern string _NativeGallery_GetImageProperties( string path ); + + [System.Runtime.InteropServices.DllImport( "__Internal" )] + private static extern string _NativeGallery_GetVideoProperties( string path ); + + [System.Runtime.InteropServices.DllImport( "__Internal" )] + private static extern string _NativeGallery_GetVideoThumbnail( string path, string thumbnailSavePath, int maxSize, double captureTimeInSeconds ); + + [System.Runtime.InteropServices.DllImport( "__Internal" )] + private static extern string _NativeGallery_LoadImageAtPath( string path, string temporaryFilePath, int maxSize ); +#endif + +#if !UNITY_EDITOR && ( UNITY_ANDROID || UNITY_IOS ) + private static string m_temporaryImagePath = null; + private static string TemporaryImagePath + { + get + { + if( m_temporaryImagePath == null ) + { + m_temporaryImagePath = Path.Combine( Application.temporaryCachePath, "tmpImg" ); + Directory.CreateDirectory( Application.temporaryCachePath ); + } + + return m_temporaryImagePath; + } + } + + private static string m_selectedMediaPath = null; + private static string SelectedMediaPath + { + get + { + if( m_selectedMediaPath == null ) + { + m_selectedMediaPath = Path.Combine( Application.temporaryCachePath, "pickedMedia" ); + Directory.CreateDirectory( Application.temporaryCachePath ); + } + + return m_selectedMediaPath; + } + } +#endif + #endregion + + #region Runtime Permissions + // PermissionFreeMode was initially planned to be a toggleable setting on iOS but it has its own issues when set to false, so its value is forced to true. + // These issues are: + // - Presented permission dialog will have a "Select Photos" option on iOS 14+ but clicking it will freeze and eventually crash the app (I'm guessing that + // this is caused by how permissions are handled synchronously in NativeGallery) + // - While saving images/videos to Photos, iOS 14+ users would see the "Select Photos" option (which is irrelevant in this context, hence confusing) and + // the user must grant full Photos access in order to save the image/video to a custom album + // The only downside of having PermissionFreeMode = true is that, on iOS 14+, images/videos will be saved to the default Photos album rather than the + // provided custom album + private const bool PermissionFreeMode = true; + + public static Permission CheckPermission( PermissionType permissionType, MediaType mediaTypes ) + { +#if !UNITY_EDITOR && UNITY_ANDROID + Permission result = (Permission) AJC.CallStatic( "CheckPermission", Context, permissionType == PermissionType.Read, (int) mediaTypes ); + if( result == Permission.Denied && (Permission) PlayerPrefs.GetInt( "NativeGalleryPermission", (int) Permission.ShouldAsk ) == Permission.ShouldAsk ) + result = Permission.ShouldAsk; + + return result; +#elif !UNITY_EDITOR && UNITY_IOS + return ProcessPermission( (Permission) _NativeGallery_CheckPermission( permissionType == PermissionType.Read ? 1 : 0, PermissionFreeMode ? 1 : 0 ) ); +#else + return Permission.Granted; +#endif + } + + public static Permission RequestPermission( PermissionType permissionType, MediaType mediaTypes ) + { + // Don't block the main thread if the permission is already granted + if( CheckPermission( permissionType, mediaTypes ) == Permission.Granted ) + return Permission.Granted; + +#if !UNITY_EDITOR && UNITY_ANDROID + object threadLock = new object(); + lock( threadLock ) + { + NGPermissionCallbackAndroid nativeCallback = new NGPermissionCallbackAndroid( threadLock ); + + AJC.CallStatic( "RequestPermission", Context, nativeCallback, permissionType == PermissionType.Read, (int) mediaTypes, (int) Permission.ShouldAsk ); + + if( nativeCallback.Result == -1 ) + System.Threading.Monitor.Wait( threadLock ); + + if( (Permission) nativeCallback.Result != Permission.ShouldAsk && PlayerPrefs.GetInt( "NativeGalleryPermission", -1 ) != nativeCallback.Result ) + { + PlayerPrefs.SetInt( "NativeGalleryPermission", nativeCallback.Result ); + PlayerPrefs.Save(); + } + + return (Permission) nativeCallback.Result; + } +#elif !UNITY_EDITOR && UNITY_IOS + return ProcessPermission( (Permission) _NativeGallery_RequestPermission( permissionType == PermissionType.Read ? 1 : 0, PermissionFreeMode ? 1 : 0, 0 ) ); +#else + return Permission.Granted; +#endif + } + + public static void RequestPermissionAsync( PermissionCallback callback, PermissionType permissionType, MediaType mediaTypes ) + { +#if !UNITY_EDITOR && UNITY_ANDROID + NGPermissionCallbackAsyncAndroid nativeCallback = new NGPermissionCallbackAsyncAndroid( callback ); + AJC.CallStatic( "RequestPermission", Context, nativeCallback, permissionType == PermissionType.Read, (int) mediaTypes, (int) Permission.ShouldAsk ); +#elif !UNITY_EDITOR && UNITY_IOS + NGPermissionCallbackiOS.Initialize( ( result ) => callback( ProcessPermission( result ) ) ); + _NativeGallery_RequestPermission( permissionType == PermissionType.Read ? 1 : 0, PermissionFreeMode ? 1 : 0, 1 ); +#else + callback( Permission.Granted ); +#endif + } + +#if UNITY_2018_4_OR_NEWER && !NATIVE_GALLERY_DISABLE_ASYNC_FUNCTIONS + public static Task RequestPermissionAsync( PermissionType permissionType, MediaType mediaTypes ) + { + TaskCompletionSource tcs = new TaskCompletionSource(); + RequestPermissionAsync( ( permission ) => tcs.SetResult( permission ), permissionType, mediaTypes ); + return tcs.Task; + } +#endif + + private static Permission ProcessPermission( Permission permission ) + { + // result == 3: LimitedAccess permission on iOS, no need to handle it when PermissionFreeMode is set to true + return ( PermissionFreeMode && (int) permission == 3 ) ? Permission.Granted : permission; + } + + // This function isn't needed when PermissionFreeMode is set to true + private static void TryExtendLimitedAccessPermission() + { + if( IsMediaPickerBusy() ) + return; + +#if !UNITY_EDITOR && UNITY_IOS + _NativeGallery_ShowLimitedLibraryPicker(); +#endif + } + + public static bool CanOpenSettings() + { +#if !UNITY_EDITOR && UNITY_IOS + return _NativeGallery_CanOpenSettings() == 1; +#else + return true; +#endif + } + + public static void OpenSettings() + { +#if !UNITY_EDITOR && UNITY_ANDROID + AJC.CallStatic( "OpenSettings", Context ); +#elif !UNITY_EDITOR && UNITY_IOS + _NativeGallery_OpenSettings(); +#endif + } + #endregion + + #region Save Functions + public static Permission SaveImageToGallery( byte[] mediaBytes, string album, string filename, MediaSaveCallback callback = null ) + { + return SaveToGallery( mediaBytes, album, filename, MediaType.Image, callback ); + } + + public static Permission SaveImageToGallery( string existingMediaPath, string album, string filename, MediaSaveCallback callback = null ) + { + return SaveToGallery( existingMediaPath, album, filename, MediaType.Image, callback ); + } + + public static Permission SaveImageToGallery( Texture2D image, string album, string filename, MediaSaveCallback callback = null ) + { + if( image == null ) + throw new ArgumentException( "Parameter 'image' is null!" ); + + if( filename.EndsWith( ".jpeg", StringComparison.OrdinalIgnoreCase ) || filename.EndsWith( ".jpg", StringComparison.OrdinalIgnoreCase ) ) + return SaveToGallery( GetTextureBytes( image, true ), album, filename, MediaType.Image, callback ); + else if( filename.EndsWith( ".png", StringComparison.OrdinalIgnoreCase ) ) + return SaveToGallery( GetTextureBytes( image, false ), album, filename, MediaType.Image, callback ); + else + return SaveToGallery( GetTextureBytes( image, false ), album, filename + ".png", MediaType.Image, callback ); + } + + public static Permission SaveVideoToGallery( byte[] mediaBytes, string album, string filename, MediaSaveCallback callback = null ) + { + return SaveToGallery( mediaBytes, album, filename, MediaType.Video, callback ); + } + + public static Permission SaveVideoToGallery( string existingMediaPath, string album, string filename, MediaSaveCallback callback = null ) + { + return SaveToGallery( existingMediaPath, album, filename, MediaType.Video, callback ); + } + + private static Permission SaveAudioToGallery( byte[] mediaBytes, string album, string filename, MediaSaveCallback callback = null ) + { + return SaveToGallery( mediaBytes, album, filename, MediaType.Audio, callback ); + } + + private static Permission SaveAudioToGallery( string existingMediaPath, string album, string filename, MediaSaveCallback callback = null ) + { + return SaveToGallery( existingMediaPath, album, filename, MediaType.Audio, callback ); + } + #endregion + + #region Load Functions + public static bool CanSelectMultipleFilesFromGallery() + { +#if !UNITY_EDITOR && UNITY_ANDROID + return AJC.CallStatic( "CanSelectMultipleMedia" ); +#elif !UNITY_EDITOR && UNITY_IOS + return _NativeGallery_CanPickMultipleMedia() == 1; +#else + return false; +#endif + } + + public static bool CanSelectMultipleMediaTypesFromGallery() + { +#if UNITY_EDITOR + return true; +#elif UNITY_ANDROID + return AJC.CallStatic( "CanSelectMultipleMediaTypes" ); +#elif UNITY_IOS + return true; +#else + return false; +#endif + } + + public static Permission GetImageFromGallery( MediaPickCallback callback, string title = "", string mime = "image/*" ) + { + return GetMediaFromGallery( callback, MediaType.Image, mime, title ); + } + + public static Permission GetVideoFromGallery( MediaPickCallback callback, string title = "", string mime = "video/*" ) + { + return GetMediaFromGallery( callback, MediaType.Video, mime, title ); + } + + public static Permission GetAudioFromGallery( MediaPickCallback callback, string title = "", string mime = "audio/*" ) + { + return GetMediaFromGallery( callback, MediaType.Audio, mime, title ); + } + + public static Permission GetMixedMediaFromGallery( MediaPickCallback callback, MediaType mediaTypes, string title = "" ) + { + return GetMediaFromGallery( callback, mediaTypes, "*/*", title ); + } + + public static Permission GetImagesFromGallery( MediaPickMultipleCallback callback, string title = "", string mime = "image/*" ) + { + return GetMultipleMediaFromGallery( callback, MediaType.Image, mime, title ); + } + + public static Permission GetVideosFromGallery( MediaPickMultipleCallback callback, string title = "", string mime = "video/*" ) + { + return GetMultipleMediaFromGallery( callback, MediaType.Video, mime, title ); + } + + public static Permission GetAudiosFromGallery( MediaPickMultipleCallback callback, string title = "", string mime = "audio/*" ) + { + return GetMultipleMediaFromGallery( callback, MediaType.Audio, mime, title ); + } + + public static Permission GetMixedMediasFromGallery( MediaPickMultipleCallback callback, MediaType mediaTypes, string title = "" ) + { + return GetMultipleMediaFromGallery( callback, mediaTypes, "*/*", title ); + } + + public static bool IsMediaPickerBusy() + { +#if !UNITY_EDITOR && UNITY_IOS + return NGMediaReceiveCallbackiOS.IsBusy; +#else + return false; +#endif + } + + public static MediaType GetMediaTypeOfFile( string path ) + { + if( string.IsNullOrEmpty( path ) ) + return (MediaType) 0; + + string extension = Path.GetExtension( path ); + if( string.IsNullOrEmpty( extension ) ) + return (MediaType) 0; + + if( extension[0] == '.' ) + { + if( extension.Length == 1 ) + return (MediaType) 0; + + extension = extension.Substring( 1 ); + } + +#if UNITY_EDITOR + extension = extension.ToLowerInvariant(); + if( extension == "png" || extension == "jpg" || extension == "jpeg" || extension == "gif" || extension == "bmp" || extension == "tiff" ) + return MediaType.Image; + else if( extension == "mp4" || extension == "mov" || extension == "wav" || extension == "avi" ) + return MediaType.Video; + else if( extension == "mp3" || extension == "aac" || extension == "flac" ) + return MediaType.Audio; + + return (MediaType) 0; +#elif UNITY_ANDROID + string mime = AJC.CallStatic( "GetMimeTypeFromExtension", extension.ToLowerInvariant() ); + if( string.IsNullOrEmpty( mime ) ) + return (MediaType) 0; + else if( mime.StartsWith( "image/" ) ) + return MediaType.Image; + else if( mime.StartsWith( "video/" ) ) + return MediaType.Video; + else if( mime.StartsWith( "audio/" ) ) + return MediaType.Audio; + else + return (MediaType) 0; +#elif UNITY_IOS + return (MediaType) _NativeGallery_GetMediaTypeFromExtension( extension.ToLowerInvariant() ); +#else + return (MediaType) 0; +#endif + } + #endregion + + #region Internal Functions + private static Permission SaveToGallery( byte[] mediaBytes, string album, string filename, MediaType mediaType, MediaSaveCallback callback ) + { + Permission result = RequestPermission( PermissionType.Write, mediaType ); + if( result == Permission.Granted ) + { + if( mediaBytes == null || mediaBytes.Length == 0 ) + throw new ArgumentException( "Parameter 'mediaBytes' is null or empty!" ); + + if( album == null || album.Length == 0 ) + throw new ArgumentException( "Parameter 'album' is null or empty!" ); + + if( filename == null || filename.Length == 0 ) + throw new ArgumentException( "Parameter 'filename' is null or empty!" ); + + if( string.IsNullOrEmpty( Path.GetExtension( filename ) ) ) + Debug.LogWarning( "'filename' doesn't have an extension, this might result in unexpected behaviour!" ); + + string path = GetTemporarySavePath( filename ); +#if UNITY_EDITOR + Debug.Log( "SaveToGallery called successfully in the Editor" ); +#else + File.WriteAllBytes( path, mediaBytes ); +#endif + + SaveToGalleryInternal( path, album, mediaType, callback ); + } + + return result; + } + + private static Permission SaveToGallery( string existingMediaPath, string album, string filename, MediaType mediaType, MediaSaveCallback callback ) + { + Permission result = RequestPermission( PermissionType.Write, mediaType ); + if( result == Permission.Granted ) + { + if( !File.Exists( existingMediaPath ) ) + throw new FileNotFoundException( "File not found at " + existingMediaPath ); + + if( album == null || album.Length == 0 ) + throw new ArgumentException( "Parameter 'album' is null or empty!" ); + + if( filename == null || filename.Length == 0 ) + throw new ArgumentException( "Parameter 'filename' is null or empty!" ); + + if( string.IsNullOrEmpty( Path.GetExtension( filename ) ) ) + { + string originalExtension = Path.GetExtension( existingMediaPath ); + if( string.IsNullOrEmpty( originalExtension ) ) + Debug.LogWarning( "'filename' doesn't have an extension, this might result in unexpected behaviour!" ); + else + filename += originalExtension; + } + + string path = GetTemporarySavePath( filename ); +#if UNITY_EDITOR + Debug.Log( "SaveToGallery called successfully in the Editor" ); +#else + File.Copy( existingMediaPath, path, true ); +#endif + + SaveToGalleryInternal( path, album, mediaType, callback ); + } + + return result; + } + + private static void SaveToGalleryInternal( string path, string album, MediaType mediaType, MediaSaveCallback callback ) + { +#if !UNITY_EDITOR && UNITY_ANDROID + string savePath = AJC.CallStatic( "SaveMedia", Context, (int) mediaType, path, album ); + + File.Delete( path ); + + if( callback != null ) + callback( !string.IsNullOrEmpty( savePath ), savePath ); +#elif !UNITY_EDITOR && UNITY_IOS + if( mediaType == MediaType.Audio ) + { + Debug.LogError( "Saving audio files is not supported on iOS" ); + + if( callback != null ) + callback( false, null ); + + return; + } + + Debug.Log( "Saving to Pictures: " + Path.GetFileName( path ) ); + + NGMediaSaveCallbackiOS.Initialize( callback ); + if( mediaType == MediaType.Image ) + _NativeGallery_ImageWriteToAlbum( path, album, PermissionFreeMode ? 1 : 0 ); + else if( mediaType == MediaType.Video ) + _NativeGallery_VideoWriteToAlbum( path, album, PermissionFreeMode ? 1 : 0 ); +#else + if( callback != null ) + callback( true, null ); +#endif + } + + private static string GetTemporarySavePath( string filename ) + { + string saveDir = Path.Combine( Application.persistentDataPath, "NGallery" ); + Directory.CreateDirectory( saveDir ); + +#if !UNITY_EDITOR && UNITY_IOS + // Ensure a unique temporary filename on iOS: + // iOS internally copies images/videos to Photos directory of the system, + // but the process is async. The redundant file is deleted by objective-c code + // automatically after the media is saved but while it is being saved, the file + // should NOT be overwritten. Therefore, always ensure a unique filename on iOS + string path = Path.Combine( saveDir, filename ); + if( File.Exists( path ) ) + { + int fileIndex = 0; + string filenameWithoutExtension = Path.GetFileNameWithoutExtension( filename ); + string extension = Path.GetExtension( filename ); + + do + { + path = Path.Combine( saveDir, string.Concat( filenameWithoutExtension, ++fileIndex, extension ) ); + } while( File.Exists( path ) ); + } + + return path; +#else + return Path.Combine( saveDir, filename ); +#endif + } + + private static Permission GetMediaFromGallery( MediaPickCallback callback, MediaType mediaType, string mime, string title ) + { + Permission result = RequestPermission( PermissionType.Read, mediaType ); + if( result == Permission.Granted && !IsMediaPickerBusy() ) + { +#if UNITY_EDITOR + System.Collections.Generic.List editorFilters = new System.Collections.Generic.List( 4 ); + + if( ( mediaType & MediaType.Image ) == MediaType.Image ) + { + editorFilters.Add( "Image files" ); + editorFilters.Add( "png,jpg,jpeg" ); + } + + if( ( mediaType & MediaType.Video ) == MediaType.Video ) + { + editorFilters.Add( "Video files" ); + editorFilters.Add( "mp4,mov,webm,avi" ); + } + + if( ( mediaType & MediaType.Audio ) == MediaType.Audio ) + { + editorFilters.Add( "Audio files" ); + editorFilters.Add( "mp3,wav,aac,flac" ); + } + + editorFilters.Add( "All files" ); + editorFilters.Add( "*" ); + + string pickedFile = UnityEditor.EditorUtility.OpenFilePanelWithFilters( "Select file", "", editorFilters.ToArray() ); + + if( callback != null ) + callback( pickedFile != "" ? pickedFile : null ); +#elif UNITY_ANDROID + AJC.CallStatic( "PickMedia", Context, new NGMediaReceiveCallbackAndroid( callback, null ), (int) mediaType, false, SelectedMediaPath, mime, title ); +#elif UNITY_IOS + if( mediaType == MediaType.Audio ) + { + Debug.LogError( "Picking audio files is not supported on iOS" ); + + if( callback != null ) // Selecting audio files is not supported on iOS + callback( null ); + } + else + { + NGMediaReceiveCallbackiOS.Initialize( callback, null ); + _NativeGallery_PickMedia( SelectedMediaPath, (int) ( mediaType & ~MediaType.Audio ), PermissionFreeMode ? 1 : 0, 1 ); + } +#else + if( callback != null ) + callback( null ); +#endif + } + + return result; + } + + private static Permission GetMultipleMediaFromGallery( MediaPickMultipleCallback callback, MediaType mediaType, string mime, string title ) + { + Permission result = RequestPermission( PermissionType.Read, mediaType ); + if( result == Permission.Granted && !IsMediaPickerBusy() ) + { + if( CanSelectMultipleFilesFromGallery() ) + { +#if !UNITY_EDITOR && UNITY_ANDROID + AJC.CallStatic( "PickMedia", Context, new NGMediaReceiveCallbackAndroid( null, callback ), (int) mediaType, true, SelectedMediaPath, mime, title ); +#elif !UNITY_EDITOR && UNITY_IOS + if( mediaType == MediaType.Audio ) + { + Debug.LogError( "Picking audio files is not supported on iOS" ); + + if( callback != null ) // Selecting audio files is not supported on iOS + callback( null ); + } + else + { + NGMediaReceiveCallbackiOS.Initialize( null, callback ); + _NativeGallery_PickMedia( SelectedMediaPath, (int) ( mediaType & ~MediaType.Audio ), PermissionFreeMode ? 1 : 0, 0 ); + } +#else + if( callback != null ) + callback( null ); +#endif + } + else if( callback != null ) + callback( null ); + } + + return result; + } + + private static byte[] GetTextureBytes( Texture2D texture, bool isJpeg ) + { + try + { + return isJpeg ? texture.EncodeToJPG( 100 ) : texture.EncodeToPNG(); + } + catch( UnityException ) + { + return GetTextureBytesFromCopy( texture, isJpeg ); + } + catch( ArgumentException ) + { + return GetTextureBytesFromCopy( texture, isJpeg ); + } + +#pragma warning disable 0162 + return null; +#pragma warning restore 0162 + } + + private static byte[] GetTextureBytesFromCopy( Texture2D texture, bool isJpeg ) + { + // Texture is marked as non-readable, create a readable copy and save it instead + Debug.LogWarning( "Saving non-readable textures is slower than saving readable textures" ); + + Texture2D sourceTexReadable = null; + RenderTexture rt = RenderTexture.GetTemporary( texture.width, texture.height ); + RenderTexture activeRT = RenderTexture.active; + + try + { + Graphics.Blit( texture, rt ); + RenderTexture.active = rt; + + sourceTexReadable = new Texture2D( texture.width, texture.height, isJpeg ? TextureFormat.RGB24 : TextureFormat.RGBA32, false ); + sourceTexReadable.ReadPixels( new Rect( 0, 0, texture.width, texture.height ), 0, 0, false ); + sourceTexReadable.Apply( false, false ); + } + catch( Exception e ) + { + Debug.LogException( e ); + + Object.DestroyImmediate( sourceTexReadable ); + return null; + } + finally + { + RenderTexture.active = activeRT; + RenderTexture.ReleaseTemporary( rt ); + } + + try + { + return isJpeg ? sourceTexReadable.EncodeToJPG( 100 ) : sourceTexReadable.EncodeToPNG(); + } + catch( Exception e ) + { + Debug.LogException( e ); + return null; + } + finally + { + Object.DestroyImmediate( sourceTexReadable ); + } + } + +#if UNITY_2018_4_OR_NEWER && !NATIVE_GALLERY_DISABLE_ASYNC_FUNCTIONS + private static async Task TryCallNativeAndroidFunctionOnSeparateThread( Func function ) + { + T result = default( T ); + bool hasResult = false; + + await Task.Run( () => + { + if( AndroidJNI.AttachCurrentThread() != 0 ) + Debug.LogWarning( "Couldn't attach JNI thread, calling native function on the main thread" ); + else + { + try + { + result = function(); + hasResult = true; + } + finally + { + AndroidJNI.DetachCurrentThread(); + } + } + } ); + + return hasResult ? result : function(); + } +#endif + #endregion + + #region Utility Functions + public static Texture2D LoadImageAtPath( string imagePath, int maxSize = -1, bool markTextureNonReadable = true, bool generateMipmaps = true, bool linearColorSpace = false ) + { + if( string.IsNullOrEmpty( imagePath ) ) + throw new ArgumentException( "Parameter 'imagePath' is null or empty!" ); + + if( !File.Exists( imagePath ) ) + throw new FileNotFoundException( "File not found at " + imagePath ); + + if( maxSize <= 0 ) + maxSize = SystemInfo.maxTextureSize; + +#if !UNITY_EDITOR && UNITY_ANDROID + string loadPath = AJC.CallStatic( "LoadImageAtPath", Context, imagePath, TemporaryImagePath, maxSize ); +#elif !UNITY_EDITOR && UNITY_IOS + string loadPath = _NativeGallery_LoadImageAtPath( imagePath, TemporaryImagePath, maxSize ); +#else + string loadPath = imagePath; +#endif + + string extension = Path.GetExtension( imagePath ).ToLowerInvariant(); + TextureFormat format = ( extension == ".jpg" || extension == ".jpeg" ) ? TextureFormat.RGB24 : TextureFormat.RGBA32; + + Texture2D result = new Texture2D( 2, 2, format, generateMipmaps, linearColorSpace ); + + try + { + if( !result.LoadImage( File.ReadAllBytes( loadPath ), markTextureNonReadable ) ) + { + Debug.LogWarning( "Couldn't load image at path: " + loadPath ); + + Object.DestroyImmediate( result ); + return null; + } + } + catch( Exception e ) + { + Debug.LogException( e ); + + Object.DestroyImmediate( result ); + return null; + } + finally + { + if( loadPath != imagePath ) + { + try + { + File.Delete( loadPath ); + } + catch { } + } + } + + return result; + } + +#if UNITY_2018_4_OR_NEWER && !NATIVE_GALLERY_DISABLE_ASYNC_FUNCTIONS + public static async Task LoadImageAtPathAsync( string imagePath, int maxSize = -1, bool markTextureNonReadable = true ) + { + if( string.IsNullOrEmpty( imagePath ) ) + throw new ArgumentException( "Parameter 'imagePath' is null or empty!" ); + + if( !File.Exists( imagePath ) ) + throw new FileNotFoundException( "File not found at " + imagePath ); + + if( maxSize <= 0 ) + maxSize = SystemInfo.maxTextureSize; + +#if !UNITY_EDITOR && UNITY_ANDROID + string temporaryImagePath = TemporaryImagePath; // Must be accessed from main thread + string loadPath = await TryCallNativeAndroidFunctionOnSeparateThread( () => AJC.CallStatic( "LoadImageAtPath", Context, imagePath, temporaryImagePath, maxSize ) ); +#elif !UNITY_EDITOR && UNITY_IOS + string temporaryImagePath = TemporaryImagePath; // Must be accessed from main thread + string loadPath = await Task.Run( () => _NativeGallery_LoadImageAtPath( imagePath, temporaryImagePath, maxSize ) ); +#else + string loadPath = imagePath; +#endif + + Texture2D result = null; + + using( UnityWebRequest www = UnityWebRequestTexture.GetTexture( "file://" + loadPath, markTextureNonReadable ) ) + { + UnityWebRequestAsyncOperation asyncOperation = www.SendWebRequest(); + while( !asyncOperation.isDone ) + await Task.Yield(); + +#if UNITY_2020_1_OR_NEWER + if( www.result != UnityWebRequest.Result.Success ) +#else + if( www.isNetworkError || www.isHttpError ) +#endif + { + Debug.LogWarning( "Couldn't use UnityWebRequest to load image, falling back to LoadImage: " + www.error ); + } + else + result = DownloadHandlerTexture.GetContent( www ); + } + + if( !result ) // Fallback to Texture2D.LoadImage if something goes wrong + { + string extension = Path.GetExtension( imagePath ).ToLowerInvariant(); + TextureFormat format = ( extension == ".jpg" || extension == ".jpeg" ) ? TextureFormat.RGB24 : TextureFormat.RGBA32; + + result = new Texture2D( 2, 2, format, true, false ); + + try + { + if( !result.LoadImage( File.ReadAllBytes( loadPath ), markTextureNonReadable ) ) + { + Debug.LogWarning( "Couldn't load image at path: " + loadPath ); + + Object.DestroyImmediate( result ); + return null; + } + } + catch( Exception e ) + { + Debug.LogException( e ); + + Object.DestroyImmediate( result ); + return null; + } + finally + { + if( loadPath != imagePath ) + { + try + { + File.Delete( loadPath ); + } + catch { } + } + } + } + + return result; + } +#endif + + public static Texture2D GetVideoThumbnail( string videoPath, int maxSize = -1, double captureTimeInSeconds = -1.0, bool markTextureNonReadable = true, bool generateMipmaps = true, bool linearColorSpace = false ) + { + if( maxSize <= 0 ) + maxSize = SystemInfo.maxTextureSize; + +#if !UNITY_EDITOR && UNITY_ANDROID + string thumbnailPath = AJC.CallStatic( "GetVideoThumbnail", Context, videoPath, TemporaryImagePath + ".png", false, maxSize, captureTimeInSeconds ); +#elif !UNITY_EDITOR && UNITY_IOS + string thumbnailPath = _NativeGallery_GetVideoThumbnail( videoPath, TemporaryImagePath + ".png", maxSize, captureTimeInSeconds ); +#else + string thumbnailPath = null; +#endif + + if( !string.IsNullOrEmpty( thumbnailPath ) ) + return LoadImageAtPath( thumbnailPath, maxSize, markTextureNonReadable, generateMipmaps, linearColorSpace ); + else + return null; + } + +#if UNITY_2018_4_OR_NEWER && !NATIVE_GALLERY_DISABLE_ASYNC_FUNCTIONS + public static async Task GetVideoThumbnailAsync( string videoPath, int maxSize = -1, double captureTimeInSeconds = -1.0, bool markTextureNonReadable = true ) + { + if( maxSize <= 0 ) + maxSize = SystemInfo.maxTextureSize; + +#if !UNITY_EDITOR && UNITY_ANDROID + string temporaryImagePath = TemporaryImagePath; // Must be accessed from main thread + string thumbnailPath = await TryCallNativeAndroidFunctionOnSeparateThread( () => AJC.CallStatic( "GetVideoThumbnail", Context, videoPath, temporaryImagePath + ".png", false, maxSize, captureTimeInSeconds ) ); +#elif !UNITY_EDITOR && UNITY_IOS + string temporaryImagePath = TemporaryImagePath; // Must be accessed from main thread + string thumbnailPath = await Task.Run( () => _NativeGallery_GetVideoThumbnail( videoPath, temporaryImagePath + ".png", maxSize, captureTimeInSeconds ) ); +#else + string thumbnailPath = null; +#endif + + if( !string.IsNullOrEmpty( thumbnailPath ) ) + return await LoadImageAtPathAsync( thumbnailPath, maxSize, markTextureNonReadable ); + else + return null; + } +#endif + + public static ImageProperties GetImageProperties( string imagePath ) + { + if( !File.Exists( imagePath ) ) + throw new FileNotFoundException( "File not found at " + imagePath ); + +#if !UNITY_EDITOR && UNITY_ANDROID + string value = AJC.CallStatic( "GetImageProperties", Context, imagePath ); +#elif !UNITY_EDITOR && UNITY_IOS + string value = _NativeGallery_GetImageProperties( imagePath ); +#else + string value = null; +#endif + + int width = 0, height = 0; + string mimeType = null; + ImageOrientation orientation = ImageOrientation.Unknown; + if( !string.IsNullOrEmpty( value ) ) + { + string[] properties = value.Split( '>' ); + if( properties != null && properties.Length >= 4 ) + { + if( !int.TryParse( properties[0].Trim(), out width ) ) + width = 0; + if( !int.TryParse( properties[1].Trim(), out height ) ) + height = 0; + + mimeType = properties[2].Trim(); + if( mimeType.Length == 0 ) + { + string extension = Path.GetExtension( imagePath ).ToLowerInvariant(); + if( extension == ".png" ) + mimeType = "image/png"; + else if( extension == ".jpg" || extension == ".jpeg" ) + mimeType = "image/jpeg"; + else if( extension == ".gif" ) + mimeType = "image/gif"; + else if( extension == ".bmp" ) + mimeType = "image/bmp"; + else + mimeType = null; + } + + int orientationInt; + if( int.TryParse( properties[3].Trim(), out orientationInt ) ) + orientation = (ImageOrientation) orientationInt; + } + } + + return new ImageProperties( width, height, mimeType, orientation ); + } + + public static VideoProperties GetVideoProperties( string videoPath ) + { + if( !File.Exists( videoPath ) ) + throw new FileNotFoundException( "File not found at " + videoPath ); + +#if !UNITY_EDITOR && UNITY_ANDROID + string value = AJC.CallStatic( "GetVideoProperties", Context, videoPath ); +#elif !UNITY_EDITOR && UNITY_IOS + string value = _NativeGallery_GetVideoProperties( videoPath ); +#else + string value = null; +#endif + + int width = 0, height = 0; + long duration = 0L; + float rotation = 0f; + if( !string.IsNullOrEmpty( value ) ) + { + string[] properties = value.Split( '>' ); + if( properties != null && properties.Length >= 4 ) + { + if( !int.TryParse( properties[0].Trim(), out width ) ) + width = 0; + if( !int.TryParse( properties[1].Trim(), out height ) ) + height = 0; + if( !long.TryParse( properties[2].Trim(), out duration ) ) + duration = 0L; + if( !float.TryParse( properties[3].Trim().Replace( ',', '.' ), NumberStyles.Float, CultureInfo.InvariantCulture, out rotation ) ) + rotation = 0f; + } + } + + if( rotation == -90f ) + rotation = 270f; + + return new VideoProperties( width, height, duration, rotation ); + } + #endregion +} \ No newline at end of file diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/NativeGallery.cs.meta b/TheStrongestSnail/Assets/Plugins/NativeGallery/NativeGallery.cs.meta new file mode 100644 index 0000000..95d7159 --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/NativeGallery.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: ce1403606c3629046a0147d3e705f7cc +timeCreated: 1498722610 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/README.txt b/TheStrongestSnail/Assets/Plugins/NativeGallery/README.txt new file mode 100644 index 0000000..0dd4964 --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/README.txt @@ -0,0 +1,6 @@ += Native Gallery for Android & iOS (v1.8.1) = + +Documentation: https://github.com/yasirkula/UnityNativeGallery +FAQ: https://github.com/yasirkula/UnityNativeGallery#faq +Example code: https://github.com/yasirkula/UnityNativeGallery#example-code +E-mail: yasirkula@gmail.com \ No newline at end of file diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/README.txt.meta b/TheStrongestSnail/Assets/Plugins/NativeGallery/README.txt.meta new file mode 100644 index 0000000..4b9274d --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/README.txt.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: be769f45b807c40459e5bafb18e887d6 +timeCreated: 1563308465 +licenseType: Store +TextScriptImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS.meta b/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS.meta new file mode 100644 index 0000000..0474d23 --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 9c623599351a41a4c84c20f73c9d8976 +folderAsset: yes +timeCreated: 1498722622 +licenseType: Store +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGMediaReceiveCallbackiOS.cs b/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGMediaReceiveCallbackiOS.cs new file mode 100644 index 0000000..e30081f --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGMediaReceiveCallbackiOS.cs @@ -0,0 +1,132 @@ +#if UNITY_EDITOR || UNITY_IOS +using UnityEngine; + +namespace NativeGalleryNamespace +{ + public class NGMediaReceiveCallbackiOS : MonoBehaviour + { + private static NGMediaReceiveCallbackiOS instance; + + private NativeGallery.MediaPickCallback callback; + private NativeGallery.MediaPickMultipleCallback callbackMultiple; + + private float nextBusyCheckTime; + + public static bool IsBusy { get; private set; } + + [System.Runtime.InteropServices.DllImport( "__Internal" )] + private static extern int _NativeGallery_IsMediaPickerBusy(); + + public static void Initialize( NativeGallery.MediaPickCallback callback, NativeGallery.MediaPickMultipleCallback callbackMultiple ) + { + if( IsBusy ) + return; + + if( instance == null ) + { + instance = new GameObject( "NGMediaReceiveCallbackiOS" ).AddComponent(); + DontDestroyOnLoad( instance.gameObject ); + } + + instance.callback = callback; + instance.callbackMultiple = callbackMultiple; + + instance.nextBusyCheckTime = Time.realtimeSinceStartup + 1f; + IsBusy = true; + } + + private void Update() + { + if( IsBusy ) + { + if( Time.realtimeSinceStartup >= nextBusyCheckTime ) + { + nextBusyCheckTime = Time.realtimeSinceStartup + 1f; + + if( _NativeGallery_IsMediaPickerBusy() == 0 ) + { + IsBusy = false; + + NativeGallery.MediaPickCallback _callback = callback; + callback = null; + + NativeGallery.MediaPickMultipleCallback _callbackMultiple = callbackMultiple; + callbackMultiple = null; + + if( _callback != null ) + _callback( null ); + + if( _callbackMultiple != null ) + _callbackMultiple( null ); + } + } + } + } + + [UnityEngine.Scripting.Preserve] + public void OnMediaReceived( string path ) + { + IsBusy = false; + + if( string.IsNullOrEmpty( path ) ) + path = null; + + NativeGallery.MediaPickCallback _callback = callback; + callback = null; + + if( _callback != null ) + _callback( path ); + } + + [UnityEngine.Scripting.Preserve] + public void OnMultipleMediaReceived( string paths ) + { + IsBusy = false; + + string[] _paths = SplitPaths( paths ); + if( _paths != null && _paths.Length == 0 ) + _paths = null; + + NativeGallery.MediaPickMultipleCallback _callbackMultiple = callbackMultiple; + callbackMultiple = null; + + if( _callbackMultiple != null ) + _callbackMultiple( _paths ); + } + + private string[] SplitPaths( string paths ) + { + string[] result = null; + if( !string.IsNullOrEmpty( paths ) ) + { + string[] pathsSplit = paths.Split( '>' ); + + int validPathCount = 0; + for( int i = 0; i < pathsSplit.Length; i++ ) + { + if( !string.IsNullOrEmpty( pathsSplit[i] ) ) + validPathCount++; + } + + if( validPathCount == 0 ) + pathsSplit = new string[0]; + else if( validPathCount != pathsSplit.Length ) + { + string[] validPaths = new string[validPathCount]; + for( int i = 0, j = 0; i < pathsSplit.Length; i++ ) + { + if( !string.IsNullOrEmpty( pathsSplit[i] ) ) + validPaths[j++] = pathsSplit[i]; + } + + pathsSplit = validPaths; + } + + result = pathsSplit; + } + + return result; + } + } +} +#endif \ No newline at end of file diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGMediaReceiveCallbackiOS.cs.meta b/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGMediaReceiveCallbackiOS.cs.meta new file mode 100644 index 0000000..3ebbab4 --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGMediaReceiveCallbackiOS.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 71fb861c149c2d1428544c601e52a33c +timeCreated: 1519060539 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGMediaSaveCallbackiOS.cs b/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGMediaSaveCallbackiOS.cs new file mode 100644 index 0000000..222965b --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGMediaSaveCallbackiOS.cs @@ -0,0 +1,45 @@ +#if UNITY_EDITOR || UNITY_IOS +using UnityEngine; + +namespace NativeGalleryNamespace +{ + public class NGMediaSaveCallbackiOS : MonoBehaviour + { + private static NGMediaSaveCallbackiOS instance; + private NativeGallery.MediaSaveCallback callback; + + public static void Initialize( NativeGallery.MediaSaveCallback callback ) + { + if( instance == null ) + { + instance = new GameObject( "NGMediaSaveCallbackiOS" ).AddComponent(); + DontDestroyOnLoad( instance.gameObject ); + } + else if( instance.callback != null ) + instance.callback( false, null ); + + instance.callback = callback; + } + + [UnityEngine.Scripting.Preserve] + public void OnMediaSaveCompleted( string message ) + { + NativeGallery.MediaSaveCallback _callback = callback; + callback = null; + + if( _callback != null ) + _callback( true, null ); + } + + [UnityEngine.Scripting.Preserve] + public void OnMediaSaveFailed( string error ) + { + NativeGallery.MediaSaveCallback _callback = callback; + callback = null; + + if( _callback != null ) + _callback( false, null ); + } + } +} +#endif \ No newline at end of file diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGMediaSaveCallbackiOS.cs.meta b/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGMediaSaveCallbackiOS.cs.meta new file mode 100644 index 0000000..be840a4 --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGMediaSaveCallbackiOS.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 9cbb865d0913a0d47bb6d2eb3ad04c4f +timeCreated: 1519060539 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGPermissionCallbackiOS.cs b/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGPermissionCallbackiOS.cs new file mode 100644 index 0000000..1936f76 --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGPermissionCallbackiOS.cs @@ -0,0 +1,35 @@ +#if UNITY_EDITOR || UNITY_IOS +using UnityEngine; + +namespace NativeGalleryNamespace +{ + public class NGPermissionCallbackiOS : MonoBehaviour + { + private static NGPermissionCallbackiOS instance; + private NativeGallery.PermissionCallback callback; + + public static void Initialize( NativeGallery.PermissionCallback callback ) + { + if( instance == null ) + { + instance = new GameObject( "NGPermissionCallbackiOS" ).AddComponent(); + DontDestroyOnLoad( instance.gameObject ); + } + else if( instance.callback != null ) + instance.callback( NativeGallery.Permission.ShouldAsk ); + + instance.callback = callback; + } + + [UnityEngine.Scripting.Preserve] + public void OnPermissionRequested( string message ) + { + NativeGallery.PermissionCallback _callback = callback; + callback = null; + + if( _callback != null ) + _callback( (NativeGallery.Permission) int.Parse( message ) ); + } + } +} +#endif \ No newline at end of file diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGPermissionCallbackiOS.cs.meta b/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGPermissionCallbackiOS.cs.meta new file mode 100644 index 0000000..d62f57d --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGPermissionCallbackiOS.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: bc6d7fa0a99114a45b1a6800097c6eb1 +timeCreated: 1519060539 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NativeGallery.mm b/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NativeGallery.mm new file mode 100644 index 0000000..8655160 --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NativeGallery.mm @@ -0,0 +1,1589 @@ +#import +#import +#import +#import +#import +#if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000 +#import +#endif +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 +#import +#endif + +#ifdef UNITY_4_0 || UNITY_5_0 +#import "iPhone_View.h" +#else +extern 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 UNativeGallery + +static NSString *pickedMediaSavePath; +static UIPopoverController *popup; +static UIImagePickerController *imagePicker; +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 +static PHPickerViewController *imagePickerNew; +#endif +static int imagePickerState = 0; // 0 -> none, 1 -> showing (always in this state on iPad), 2 -> finished +static 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]; + } +#endif + + if( asyncMode && result >= 0 ) // Result returned immediately, forward it + UnitySendMessage( "NGPermissionCallbackiOS", "OnPermissionRequested", [self getCString:[NSString stringWithFormat:@"%d", result]] ); + + return result; +} + +#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 *)results +{ + imagePickerNew = nil; + imagePickerState = 2; + + [picker dismissViewControllerAnimated:NO completion:nil]; + + if( results != nil && [results count] > 0 ) + { + NSMutableArray *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 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 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 object, NSError *error ) + { + if( object != nil && [object isKindOfClass:[PHLivePhoto class]] ) + { + // Extract image data from live photo + // Credit: https://stackoverflow.com/a/41341675/2373034 + NSArray* 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* 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; +} + +@end + +extern "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]; +} \ No newline at end of file diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NativeGallery.mm.meta b/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NativeGallery.mm.meta new file mode 100644 index 0000000..1823903 --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NativeGallery.mm.meta @@ -0,0 +1,33 @@ +fileFormatVersion: 2 +guid: 953e0b740eb03144883db35f72cad8a6 +timeCreated: 1498722774 +licenseType: Store +PluginImporter: + serializedVersion: 2 + iconMap: {} + executionOrder: {} + isPreloaded: 0 + isOverridable: 0 + platformData: + data: + first: + Any: + second: + enabled: 0 + settings: {} + data: + first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + data: + first: + iPhone: iOS + second: + enabled: 1 + settings: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/TheStrongestSnail/Assets/Scripts/LqUiScripts/EditPanel.cs b/TheStrongestSnail/Assets/Scripts/LqUiScripts/EditPanel.cs index 86b312a..bf29870 100644 --- a/TheStrongestSnail/Assets/Scripts/LqUiScripts/EditPanel.cs +++ b/TheStrongestSnail/Assets/Scripts/LqUiScripts/EditPanel.cs @@ -12,6 +12,8 @@ public class EditPanel : Base public InputField UserInputField; public Text username; public PerSonalCenterPanel perSonalCenterPanel; + + public Button avatar; // Start is called before the first frame update async void Start() { @@ -26,8 +28,50 @@ public class EditPanel : Base userInfomation14 = JsonConvert.DeserializeObject(response14); username.text = "" + userInfomation14.data.nickName; + avatar.onClick.AddListener(selectavatar); } + void selectavatar() + { + PickImage(512); + } + private void PickImage(int maxSize) + { + NativeGallery.Permission permission = NativeGallery.GetImageFromGallery((path) => + { + Debug.Log("Image path: " + path); + if (path != null) + { + // Create Texture from selected image + Texture2D texture = NativeGallery.LoadImageAtPath(path, maxSize); + if (texture == null) + { + Debug.Log("Couldn't load texture from " + path); + return; + } + + // Assign texture to a temporary quad and destroy it after 5 seconds + GameObject quad = GameObject.CreatePrimitive(PrimitiveType.Quad); + quad.transform.position = Camera.main.transform.position + Camera.main.transform.forward * 2.5f; + quad.transform.forward = Camera.main.transform.forward; + quad.transform.localScale = new Vector3(1f, texture.height / (float)texture.width, 1f); + + Material material = quad.GetComponent().material; + if (!material.shader.isSupported) // happens when Standard shader is not included in the build + material.shader = Shader.Find("Legacy Shaders/Diffuse"); + + material.mainTexture = texture; + + Destroy(quad, 5f); + + // If a procedural texture is not destroyed manually, + // it will only be freed after a scene change + Destroy(texture, 5f); + } + }); + + Debug.Log("Permission result: " + permission); + } // Update is called once per frame void Update() { diff --git a/TheStrongestSnail/Assets/prefabs/HYLPrefabs/PersonalCenterPanel.prefab b/TheStrongestSnail/Assets/prefabs/HYLPrefabs/PersonalCenterPanel.prefab index 2a2ca47..4632a2b 100644 --- a/TheStrongestSnail/Assets/prefabs/HYLPrefabs/PersonalCenterPanel.prefab +++ b/TheStrongestSnail/Assets/prefabs/HYLPrefabs/PersonalCenterPanel.prefab @@ -2069,7 +2069,6 @@ GameObject: m_Component: - component: {fileID: 6661102261700243289} - component: {fileID: 3692270011235859773} - - component: {fileID: 1821891998228383241} - component: {fileID: 4775368350903003862} m_Layer: 5 m_Name: PersonalCenterPanel @@ -2112,36 +2111,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 3661722200123750880} m_CullTransparentMesh: 1 ---- !u!114 &1821891998228383241 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 3661722200123750880} - m_Enabled: 0 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} - m_Name: - m_EditorClassIdentifier: - m_Material: {fileID: 0} - m_Color: {r: 1, g: 1, b: 1, a: 0} - m_RaycastTarget: 1 - m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} - m_Maskable: 1 - m_OnCullStateChanged: - m_PersistentCalls: - m_Calls: [] - m_Sprite: {fileID: 21300000, guid: c47381d146953bf419405b6a9a2f3f34, type: 3} - m_Type: 0 - m_PreserveAspect: 0 - m_FillCenter: 1 - m_FillMethod: 4 - m_FillAmount: 1 - m_FillClockwise: 1 - m_FillOrigin: 0 - m_UseSpriteMesh: 0 - m_PixelsPerUnitMultiplier: 1 --- !u!114 &4775368350903003862 MonoBehaviour: m_ObjectHideFlags: 0 diff --git a/TheStrongestSnail/Assets/prefabs/ge_ren_zhong_xing/EditPanel.prefab b/TheStrongestSnail/Assets/prefabs/ge_ren_zhong_xing/EditPanel.prefab index 3aa9e3a..f849966 100644 --- a/TheStrongestSnail/Assets/prefabs/ge_ren_zhong_xing/EditPanel.prefab +++ b/TheStrongestSnail/Assets/prefabs/ge_ren_zhong_xing/EditPanel.prefab @@ -468,6 +468,7 @@ MonoBehaviour: UserInputField: {fileID: 379801035829787383} username: {fileID: 8468291165135306074} perSonalCenterPanel: {fileID: 0} + avatar: {fileID: 2339787961763677450} --- !u!1 &6960989442929760420 GameObject: m_ObjectHideFlags: 0 @@ -752,6 +753,7 @@ GameObject: - component: {fileID: 8996637367871962009} - component: {fileID: 8996637367871962011} - component: {fileID: 8996637367871962010} + - component: {fileID: 2339787961763677450} m_Layer: 5 m_Name: Image m_TagString: Untagged @@ -817,6 +819,50 @@ MonoBehaviour: m_FillOrigin: 0 m_UseSpriteMesh: 0 m_PixelsPerUnitMultiplier: 1 +--- !u!114 &2339787961763677450 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8996637367871962008} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 8996637367871962010} + m_OnClick: + m_PersistentCalls: + m_Calls: [] --- !u!1 &8996637367919174697 GameObject: m_ObjectHideFlags: 0