浏览代码

更换保存图片到相册的视线方式

guodong 1 年之前
父节点
当前提交
581a83e831
共有 35 个文件被更改,包括 3476 次插入71 次删除
  1. 2 1
      GameClient/Assets/Game/HotUpdate/Game.HotUpdate.asmdef
  2. 98 37
      GameClient/Assets/Game/HotUpdate/Views/DressUp/PhotographSaveView.cs
  3. 1 1
      GameClient/Assets/Plugins/Android/AndroidManifest.xml
  4. 二进制
      GameClient/Assets/Plugins/Android/gfglibrary-debug.aar
  5. 0 32
      GameClient/Assets/Plugins/Android/gfglibrary-debug.aar.meta
  6. 9 0
      GameClient/Assets/Plugins/NativeGallery.meta
  7. 9 0
      GameClient/Assets/Plugins/NativeGallery/Android.meta
  8. 38 0
      GameClient/Assets/Plugins/NativeGallery/Android/NGCallbackHelper.cs
  9. 12 0
      GameClient/Assets/Plugins/NativeGallery/Android/NGCallbackHelper.cs.meta
  10. 60 0
      GameClient/Assets/Plugins/NativeGallery/Android/NGMediaReceiveCallbackAndroid.cs
  11. 12 0
      GameClient/Assets/Plugins/NativeGallery/Android/NGMediaReceiveCallbackAndroid.cs.meta
  12. 46 0
      GameClient/Assets/Plugins/NativeGallery/Android/NGPermissionCallbackAndroid.cs
  13. 12 0
      GameClient/Assets/Plugins/NativeGallery/Android/NGPermissionCallbackAndroid.cs.meta
  14. 二进制
      GameClient/Assets/Plugins/NativeGallery/Android/NativeGallery.aar
  15. 33 0
      GameClient/Assets/Plugins/NativeGallery/Android/NativeGallery.aar.meta
  16. 9 0
      GameClient/Assets/Plugins/NativeGallery/Editor.meta
  17. 152 0
      GameClient/Assets/Plugins/NativeGallery/Editor/NGPostProcessBuild.cs
  18. 12 0
      GameClient/Assets/Plugins/NativeGallery/Editor/NGPostProcessBuild.cs.meta
  19. 15 0
      GameClient/Assets/Plugins/NativeGallery/Editor/NativeGallery.Editor.asmdef
  20. 7 0
      GameClient/Assets/Plugins/NativeGallery/Editor/NativeGallery.Editor.asmdef.meta
  21. 3 0
      GameClient/Assets/Plugins/NativeGallery/NativeGallery.Runtime.asmdef
  22. 7 0
      GameClient/Assets/Plugins/NativeGallery/NativeGallery.Runtime.asmdef.meta
  23. 1039 0
      GameClient/Assets/Plugins/NativeGallery/NativeGallery.cs
  24. 12 0
      GameClient/Assets/Plugins/NativeGallery/NativeGallery.cs.meta
  25. 6 0
      GameClient/Assets/Plugins/NativeGallery/README.txt
  26. 8 0
      GameClient/Assets/Plugins/NativeGallery/README.txt.meta
  27. 9 0
      GameClient/Assets/Plugins/NativeGallery/iOS.meta
  28. 130 0
      GameClient/Assets/Plugins/NativeGallery/iOS/NGMediaReceiveCallbackiOS.cs
  29. 12 0
      GameClient/Assets/Plugins/NativeGallery/iOS/NGMediaReceiveCallbackiOS.cs.meta
  30. 43 0
      GameClient/Assets/Plugins/NativeGallery/iOS/NGMediaSaveCallbackiOS.cs
  31. 12 0
      GameClient/Assets/Plugins/NativeGallery/iOS/NGMediaSaveCallbackiOS.cs.meta
  32. 34 0
      GameClient/Assets/Plugins/NativeGallery/iOS/NGPermissionCallbackiOS.cs
  33. 12 0
      GameClient/Assets/Plugins/NativeGallery/iOS/NGPermissionCallbackiOS.cs.meta
  34. 1589 0
      GameClient/Assets/Plugins/NativeGallery/iOS/NativeGallery.mm
  35. 33 0
      GameClient/Assets/Plugins/NativeGallery/iOS/NativeGallery.mm.meta

+ 2 - 1
GameClient/Assets/Game/HotUpdate/Game.HotUpdate.asmdef

@@ -5,7 +5,8 @@
         "GUID:065d51621aad9d6498c45bc251a1d8e4",
         "GUID:7a41fac89c3ce014e99efb3723e6a98e",
         "GUID:e34a5702dd353724aa315fb8011f08c3",
-        "GUID:b0a84d582f6a6fa4185f67ce934d99c2"
+        "GUID:b0a84d582f6a6fa4185f67ce934d99c2",
+        "GUID:6e5063adab271564ba0098a06a8cebda"
     ],
     "includePlatforms": [],
     "excludePlatforms": [],

+ 98 - 37
GameClient/Assets/Game/HotUpdate/Views/DressUp/PhotographSaveView.cs

@@ -1,8 +1,4 @@
-using System;
-using System.Collections;
 using System.Collections.Generic;
-using System.IO;
-using System.Threading.Tasks;
 using ET;
 using FairyGUI;
 using UI.DressUp;
@@ -65,50 +61,115 @@ namespace GFGGame
 
         private void OnClickBtnSave()
         {
-            //检查用户是否已授予对需要授权的设备资源或信息的访问权。
-            if (!Permission.HasUserAuthorizedPermission(Permission.ExternalStorageWrite))
+#if UNITY_EDITOR
+            PromptController.Instance.ShowFloatTextPrompt("此功能只能在移动端使用!");
+#else
+            CheckSaveLocal();
+#endif
+        }
+
+        private async void CheckSaveLocal()
+        {
+            string permissionName = "存储";
+#if UNITY_IOS
+                string permissionName = "相册";
+#endif
+            NativeGallery.Permission permission = NativeGallery.CheckPermission(NativeGallery.PermissionType.Write, NativeGallery.MediaType.Image);
+            Debug.Log("Permission result: " + permission);
+
+            if(permission.Equals(NativeGallery.Permission.Denied))
             {
-                if (LocalCache.GetBool(GameConst.WRITE_EXTERNAL_STORAGE_FORBIDDEN, false))
+                string tips = $"保存至本地需要使用{permissionName}权限,您已经禁止!请前往手机系统设置开启应用的{permissionName}权限。";
+                if (NativeGallery.CanOpenSettings())
                 {
-                    AlertSystem.Show("保存至本地需要使用存储权限,您已经禁止!请前往手机系统设置开启应用存储权限。")
-                            .SetLeftButton(true, "知道了");
-                    return;
+                    AlertSystem.Show(tips)
+                                .SetLeftButton(true, "前往", (data) => { NativeGallery.OpenSettings(); })
+                                .SetRightButton(true, "拒绝");
                 }
-                PromptController.Instance.ShowFloatTextPrompt("保存至本地需要使用存储权限,请同意!");
-                //请求用户授权访问需要授权的设备资源或信息.
-                PermissionCallbacks permissionCallbacks = new PermissionCallbacks();
-                permissionCallbacks.PermissionGranted += (string a) =>
-                {
-                    TrySavePicturoToLocal();
-                };
-                permissionCallbacks.PermissionDenied += (string a) =>
-                {
-                    PromptController.Instance.ShowFloatTextPrompt("由于被禁止存储权限,保存失败!");
-                };
-                permissionCallbacks.PermissionDeniedAndDontAskAgain += (string a) =>
+                else
                 {
-                    LocalCache.SetBool(GameConst.WRITE_EXTERNAL_STORAGE_FORBIDDEN, true);
-                    PromptController.Instance.ShowFloatTextPrompt("由于被禁止存储权限,保存失败!");
-                };
-                Permission.RequestUserPermission(Permission.ExternalStorageWrite, permissionCallbacks);
+                    AlertSystem.Show(tips)
+                            .SetLeftButton(true, "知道了");
+                }
             }
             else
             {
-                LocalCache.SetBool(GameConst.WRITE_EXTERNAL_STORAGE_FORBIDDEN, false);
-                TrySavePicturoToLocal();
-            }
-        }
-
-        private void TrySavePicturoToLocal()
-        {
-            if (Permission.HasUserAuthorizedPermission(Permission.ExternalStorageWrite))
-            {
+                if (permission.Equals(NativeGallery.Permission.ShouldAsk))
+                {
+                    string tips = $"保存至本地需要使用{permissionName}权限,请同意!";
+                    PromptController.Instance.ShowFloatTextPrompt(tips);
+                    permission = await NativeGallery.RequestPermissionAsync(NativeGallery.PermissionType.Write, NativeGallery.MediaType.Image);
+                    //Debug.Log("Permission result: " + permission);
+                    if(!permission.Equals(NativeGallery.Permission.Granted))
+                    {
+                        PromptController.Instance.ShowFloatTextPrompt($"由于被禁止{permissionName}权限,保存失败!");
+                        return;
+                    }
+                }
                 string fileName = "wsj" + TimeHelper.ServerNowSecs + ".jpg";
-                PhotographUtil.Instance.SavePicturoToLocal(bytes, fileName);
-                _ui.m_btnSave.enabled = false;
+                permission = NativeGallery.SaveImageToGallery(bytes, "Wanshijing", fileName, (success, path) => 
+                {
+                    if(success)
+                    {
+                        PromptController.Instance.ShowFloatTextPrompt("已保存至本地!");
+                        _ui.m_btnSave.enabled = false;
+                    }
+                    else
+                    {
+                        PromptController.Instance.ShowFloatTextPrompt("保存失败!");
+                    }
+                });
+
+                //Debug.Log("Permission result: " + permission);
             }
         }
 
+        //private void OnClickBtnSaveBackup()
+        //{
+        //    //检查用户是否已授予对需要授权的设备资源或信息的访问权。
+        //    if (!Permission.HasUserAuthorizedPermission(Permission.ExternalStorageWrite))
+        //    {
+        //        if (LocalCache.GetBool(GameConst.WRITE_EXTERNAL_STORAGE_FORBIDDEN, false))
+        //        {
+        //            AlertSystem.Show("保存至本地需要使用存储权限,您已经禁止!请前往手机系统设置开启应用存储权限。")
+        //                    .SetLeftButton(true, "知道了");
+        //            return;
+        //        }
+        //        PromptController.Instance.ShowFloatTextPrompt("保存至本地需要使用存储权限,请同意!");
+        //        //请求用户授权访问需要授权的设备资源或信息.
+        //        PermissionCallbacks permissionCallbacks = new PermissionCallbacks();
+        //        permissionCallbacks.PermissionGranted += (string a) =>
+        //        {
+        //            TrySavePicturoToLocal();
+        //        };
+        //        permissionCallbacks.PermissionDenied += (string a) =>
+        //        {
+        //            PromptController.Instance.ShowFloatTextPrompt("由于被禁止存储权限,保存失败!");
+        //        };
+        //        permissionCallbacks.PermissionDeniedAndDontAskAgain += (string a) =>
+        //        {
+        //            LocalCache.SetBool(GameConst.WRITE_EXTERNAL_STORAGE_FORBIDDEN, true);
+        //            PromptController.Instance.ShowFloatTextPrompt("由于被禁止存储权限,保存失败!");
+        //        };
+        //        Permission.RequestUserPermission(Permission.ExternalStorageWrite, permissionCallbacks);
+        //    }
+        //    else
+        //    {
+        //        LocalCache.SetBool(GameConst.WRITE_EXTERNAL_STORAGE_FORBIDDEN, false);
+        //        TrySavePicturoToLocal();
+        //    }
+        //}
+
+        //private void TrySavePicturoToLocal()
+        //{
+        //    if (Permission.HasUserAuthorizedPermission(Permission.ExternalStorageWrite))
+        //    {
+        //        string fileName = "wsj" + TimeHelper.ServerNowSecs + ".jpg";
+        //        PhotographUtil.Instance.SavePicturoToLocal(bytes, fileName);
+        //        _ui.m_btnSave.enabled = false;
+        //    }
+        //}
+
         private async void OnClickBtnSavePhoto()
         {
 

+ 1 - 1
GameClient/Assets/Plugins/Android/AndroidManifest.xml

@@ -18,7 +18,7 @@
     <application
         android:allowBackup="true"
 		android:icon="@drawable/ic_launcher"
-		android:label="万世镜内网">
+		android:label="test">
 		<activity android:name="com.unity3d.player.UnityPlayerActivity"
             android:configChanges="mnc|keyboardHidden|screenSize|orientation|keyboard"
             android:screenOrientation="portrait"

二进制
GameClient/Assets/Plugins/Android/gfglibrary-debug.aar


+ 0 - 32
GameClient/Assets/Plugins/Android/gfglibrary-debug.aar.meta

@@ -1,32 +0,0 @@
-fileFormatVersion: 2
-guid: 6b87e2a5baa5acd48a211faa07bf310a
-PluginImporter:
-  externalObjects: {}
-  serializedVersion: 2
-  iconMap: {}
-  executionOrder: {}
-  defineConstraints: []
-  isPreloaded: 0
-  isOverridable: 0
-  isExplicitlyReferenced: 0
-  validateReferences: 1
-  platformData:
-  - first:
-      Android: Android
-    second:
-      enabled: 1
-      settings: {}
-  - first:
-      Any: 
-    second:
-      enabled: 0
-      settings: {}
-  - first:
-      Editor: Editor
-    second:
-      enabled: 0
-      settings:
-        DefaultValueInitialized: true
-  userData: 
-  assetBundleName: 
-  assetBundleVariant: 

+ 9 - 0
GameClient/Assets/Plugins/NativeGallery.meta

@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 5e05ed2bddbccb94e9650efb5742e452
+folderAsset: yes
+timeCreated: 1518877529
+licenseType: Store
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 9 - 0
GameClient/Assets/Plugins/NativeGallery/Android.meta

@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 0a607dcda26e7614f86300c6ca717295
+folderAsset: yes
+timeCreated: 1498722617
+licenseType: Store
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 38 - 0
GameClient/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

+ 12 - 0
GameClient/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: 

+ 60 - 0
GameClient/Assets/Plugins/NativeGallery/Android/NGMediaReceiveCallbackAndroid.cs

@@ -0,0 +1,60 @@
+#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<NGCallbackHelper>();
+		}
+
+		public void OnMediaReceived( string path )
+		{
+			callbackHelper.CallOnMainThread( () => callback( !string.IsNullOrEmpty( path ) ? path : null ) );
+		}
+
+		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

+ 12 - 0
GameClient/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: 

+ 46 - 0
GameClient/Assets/Plugins/NativeGallery/Android/NGPermissionCallbackAndroid.cs

@@ -0,0 +1,46 @@
+#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;
+		}
+
+		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<NGCallbackHelper>();
+		}
+
+		public void OnPermissionResult( int result )
+		{
+			callbackHelper.CallOnMainThread( () => callback( (NativeGallery.Permission) result ) );
+		}
+	}
+}
+#endif

+ 12 - 0
GameClient/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: 

二进制
GameClient/Assets/Plugins/NativeGallery/Android/NativeGallery.aar


+ 33 - 0
GameClient/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: 

+ 9 - 0
GameClient/Assets/Plugins/NativeGallery/Editor.meta

@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 19fc6b8ce781591438a952d8aa9104f8
+folderAsset: yes
+timeCreated: 1521452097
+licenseType: Store
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 152 - 0
GameClient/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<Settings>( 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<string>() { "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
+	}
+}

+ 12 - 0
GameClient/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: 

+ 15 - 0
GameClient/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
+}

+ 7 - 0
GameClient/Assets/Plugins/NativeGallery/Editor/NativeGallery.Editor.asmdef.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 3dffc8e654f00c545a82d0a5274d51eb
+AssemblyDefinitionImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 3 - 0
GameClient/Assets/Plugins/NativeGallery/NativeGallery.Runtime.asmdef

@@ -0,0 +1,3 @@
+{
+	"name": "NativeGallery.Runtime"
+}

+ 7 - 0
GameClient/Assets/Plugins/NativeGallery/NativeGallery.Runtime.asmdef.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 6e5063adab271564ba0098a06a8cebda
+AssemblyDefinitionImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

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

+ 12 - 0
GameClient/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: 

+ 6 - 0
GameClient/Assets/Plugins/NativeGallery/README.txt

@@ -0,0 +1,6 @@
+= Native Gallery for Android & iOS (v1.7.8) =
+
+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

+ 8 - 0
GameClient/Assets/Plugins/NativeGallery/README.txt.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: be769f45b807c40459e5bafb18e887d6
+timeCreated: 1563308465
+licenseType: Store
+TextScriptImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 9 - 0
GameClient/Assets/Plugins/NativeGallery/iOS.meta

@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 9c623599351a41a4c84c20f73c9d8976
+folderAsset: yes
+timeCreated: 1498722622
+licenseType: Store
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 130 - 0
GameClient/Assets/Plugins/NativeGallery/iOS/NGMediaReceiveCallbackiOS.cs

@@ -0,0 +1,130 @@
+#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<NGMediaReceiveCallbackiOS>();
+				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 );
+					}
+				}
+			}
+		}
+
+		public void OnMediaReceived( string path )
+		{
+			IsBusy = false;
+
+			if( string.IsNullOrEmpty( path ) )
+				path = null;
+
+			NativeGallery.MediaPickCallback _callback = callback;
+			callback = null;
+
+			if( _callback != null )
+				_callback( path );
+		}
+
+		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

+ 12 - 0
GameClient/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: 

+ 43 - 0
GameClient/Assets/Plugins/NativeGallery/iOS/NGMediaSaveCallbackiOS.cs

@@ -0,0 +1,43 @@
+#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<NGMediaSaveCallbackiOS>();
+				DontDestroyOnLoad( instance.gameObject );
+			}
+			else if( instance.callback != null )
+				instance.callback( false, null );
+
+			instance.callback = callback;
+		}
+
+		public void OnMediaSaveCompleted( string message )
+		{
+			NativeGallery.MediaSaveCallback _callback = callback;
+			callback = null;
+
+			if( _callback != null )
+				_callback( true, null );
+		}
+
+		public void OnMediaSaveFailed( string error )
+		{
+			NativeGallery.MediaSaveCallback _callback = callback;
+			callback = null;
+
+			if( _callback != null )
+				_callback( false, null );
+		}
+	}
+}
+#endif

+ 12 - 0
GameClient/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: 

+ 34 - 0
GameClient/Assets/Plugins/NativeGallery/iOS/NGPermissionCallbackiOS.cs

@@ -0,0 +1,34 @@
+#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<NGPermissionCallbackiOS>();
+				DontDestroyOnLoad( instance.gameObject );
+			}
+			else if( instance.callback != null )
+				instance.callback( NativeGallery.Permission.ShouldAsk );
+
+			instance.callback = callback;
+		}
+
+		public void OnPermissionRequested( string message )
+		{
+			NativeGallery.PermissionCallback _callback = callback;
+			callback = null;
+
+			if( _callback != null )
+				_callback( (NativeGallery.Permission) int.Parse( message ) );
+		}
+	}
+}
+#endif

+ 12 - 0
GameClient/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: 

+ 1589 - 0
GameClient/Assets/Plugins/NativeGallery/iOS/NativeGallery.mm

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

+ 33 - 0
GameClient/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: