From b33ea25402dc89c04b032abeb9e7e5ab90fff89e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=A8=E5=8F=B7=E6=95=AC?= <153802103@qq.com> Date: Fri, 13 Dec 2024 09:59:41 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E7=94=A8=E7=9B=B8=E5=86=8C=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Assets/Plugins/NativeGallery.meta | 9 + .../Assets/Plugins/NativeGallery/Android.meta | 9 + .../NativeGallery/Android/NGCallbackHelper.cs | 38 + .../Android/NGCallbackHelper.cs.meta | 12 + .../Android/NGMediaReceiveCallbackAndroid.cs | 62 + .../NGMediaReceiveCallbackAndroid.cs.meta | 12 + .../Android/NGPermissionCallbackAndroid.cs | 48 + .../NGPermissionCallbackAndroid.cs.meta | 12 + .../NativeGallery/Android/NativeGallery.aar | Bin 0 -> 32446 bytes .../Android/NativeGallery.aar.meta | 33 + .../Assets/Plugins/NativeGallery/Editor.meta | 9 + .../Editor/NGPostProcessBuild.cs | 152 ++ .../Editor/NGPostProcessBuild.cs.meta | 12 + .../Editor/NativeGallery.Editor.asmdef | 15 + .../Editor/NativeGallery.Editor.asmdef.meta | 7 + .../NativeGallery.Runtime.asmdef | 3 + .../NativeGallery.Runtime.asmdef.meta | 7 + .../Plugins/NativeGallery/NativeGallery.cs | 1039 +++++++++++ .../NativeGallery/NativeGallery.cs.meta | 12 + .../Assets/Plugins/NativeGallery/README.txt | 6 + .../Plugins/NativeGallery/README.txt.meta | 8 + .../Assets/Plugins/NativeGallery/iOS.meta | 9 + .../iOS/NGMediaReceiveCallbackiOS.cs | 132 ++ .../iOS/NGMediaReceiveCallbackiOS.cs.meta | 12 + .../iOS/NGMediaSaveCallbackiOS.cs | 45 + .../iOS/NGMediaSaveCallbackiOS.cs.meta | 12 + .../iOS/NGPermissionCallbackiOS.cs | 35 + .../iOS/NGPermissionCallbackiOS.cs.meta | 12 + .../NativeGallery/iOS/NativeGallery.mm | 1589 +++++++++++++++++ .../NativeGallery/iOS/NativeGallery.mm.meta | 33 + .../Assets/Scripts/LqUiScripts/EditPanel.cs | 44 + .../HYLPrefabs/PersonalCenterPanel.prefab | 31 - .../ge_ren_zhong_xing/EditPanel.prefab | 46 + 33 files changed, 3474 insertions(+), 31 deletions(-) create mode 100644 TheStrongestSnail/Assets/Plugins/NativeGallery.meta create mode 100644 TheStrongestSnail/Assets/Plugins/NativeGallery/Android.meta create mode 100644 TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGCallbackHelper.cs create mode 100644 TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGCallbackHelper.cs.meta create mode 100644 TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGMediaReceiveCallbackAndroid.cs create mode 100644 TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGMediaReceiveCallbackAndroid.cs.meta create mode 100644 TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGPermissionCallbackAndroid.cs create mode 100644 TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGPermissionCallbackAndroid.cs.meta create mode 100644 TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NativeGallery.aar create mode 100644 TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NativeGallery.aar.meta create mode 100644 TheStrongestSnail/Assets/Plugins/NativeGallery/Editor.meta create mode 100644 TheStrongestSnail/Assets/Plugins/NativeGallery/Editor/NGPostProcessBuild.cs create mode 100644 TheStrongestSnail/Assets/Plugins/NativeGallery/Editor/NGPostProcessBuild.cs.meta create mode 100644 TheStrongestSnail/Assets/Plugins/NativeGallery/Editor/NativeGallery.Editor.asmdef create mode 100644 TheStrongestSnail/Assets/Plugins/NativeGallery/Editor/NativeGallery.Editor.asmdef.meta create mode 100644 TheStrongestSnail/Assets/Plugins/NativeGallery/NativeGallery.Runtime.asmdef create mode 100644 TheStrongestSnail/Assets/Plugins/NativeGallery/NativeGallery.Runtime.asmdef.meta create mode 100644 TheStrongestSnail/Assets/Plugins/NativeGallery/NativeGallery.cs create mode 100644 TheStrongestSnail/Assets/Plugins/NativeGallery/NativeGallery.cs.meta create mode 100644 TheStrongestSnail/Assets/Plugins/NativeGallery/README.txt create mode 100644 TheStrongestSnail/Assets/Plugins/NativeGallery/README.txt.meta create mode 100644 TheStrongestSnail/Assets/Plugins/NativeGallery/iOS.meta create mode 100644 TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGMediaReceiveCallbackiOS.cs create mode 100644 TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGMediaReceiveCallbackiOS.cs.meta create mode 100644 TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGMediaSaveCallbackiOS.cs create mode 100644 TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGMediaSaveCallbackiOS.cs.meta create mode 100644 TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGPermissionCallbackiOS.cs create mode 100644 TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGPermissionCallbackiOS.cs.meta create mode 100644 TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NativeGallery.mm create mode 100644 TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NativeGallery.mm.meta diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery.meta b/TheStrongestSnail/Assets/Plugins/NativeGallery.meta new file mode 100644 index 0000000..0c80a47 --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 5e05ed2bddbccb94e9650efb5742e452 +folderAsset: yes +timeCreated: 1518877529 +licenseType: Store +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/Android.meta b/TheStrongestSnail/Assets/Plugins/NativeGallery/Android.meta new file mode 100644 index 0000000..ae029ba --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/Android.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 0a607dcda26e7614f86300c6ca717295 +folderAsset: yes +timeCreated: 1498722617 +licenseType: Store +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGCallbackHelper.cs b/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGCallbackHelper.cs new file mode 100644 index 0000000..bf79515 --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGCallbackHelper.cs @@ -0,0 +1,38 @@ +#if UNITY_EDITOR || UNITY_ANDROID +using UnityEngine; + +namespace NativeGalleryNamespace +{ + public class NGCallbackHelper : MonoBehaviour + { + private System.Action mainThreadAction = null; + + private void Awake() + { + DontDestroyOnLoad( gameObject ); + } + + private void Update() + { + if( mainThreadAction != null ) + { + try + { + System.Action temp = mainThreadAction; + mainThreadAction = null; + temp(); + } + finally + { + Destroy( gameObject ); + } + } + } + + public void CallOnMainThread( System.Action function ) + { + mainThreadAction = function; + } + } +} +#endif \ No newline at end of file diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGCallbackHelper.cs.meta b/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGCallbackHelper.cs.meta new file mode 100644 index 0000000..d9323e8 --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGCallbackHelper.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 2d517fd0f2f85f24698df2775bee58e9 +timeCreated: 1544889149 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGMediaReceiveCallbackAndroid.cs b/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGMediaReceiveCallbackAndroid.cs new file mode 100644 index 0000000..f38c628 --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGMediaReceiveCallbackAndroid.cs @@ -0,0 +1,62 @@ +#if UNITY_EDITOR || UNITY_ANDROID +using UnityEngine; + +namespace NativeGalleryNamespace +{ + public class NGMediaReceiveCallbackAndroid : AndroidJavaProxy + { + private readonly NativeGallery.MediaPickCallback callback; + private readonly NativeGallery.MediaPickMultipleCallback callbackMultiple; + + private readonly NGCallbackHelper callbackHelper; + + public NGMediaReceiveCallbackAndroid( NativeGallery.MediaPickCallback callback, NativeGallery.MediaPickMultipleCallback callbackMultiple ) : base( "com.yasirkula.unity.NativeGalleryMediaReceiver" ) + { + this.callback = callback; + this.callbackMultiple = callbackMultiple; + callbackHelper = new GameObject( "NGCallbackHelper" ).AddComponent(); + } + + [UnityEngine.Scripting.Preserve] + public void OnMediaReceived( string path ) + { + callbackHelper.CallOnMainThread( () => callback( !string.IsNullOrEmpty( path ) ? path : null ) ); + } + + [UnityEngine.Scripting.Preserve] + public void OnMultipleMediaReceived( string paths ) + { + string[] result = null; + if( !string.IsNullOrEmpty( paths ) ) + { + string[] pathsSplit = paths.Split( '>' ); + + int validPathCount = 0; + for( int i = 0; i < pathsSplit.Length; i++ ) + { + if( !string.IsNullOrEmpty( pathsSplit[i] ) ) + validPathCount++; + } + + if( validPathCount == 0 ) + pathsSplit = new string[0]; + else if( validPathCount != pathsSplit.Length ) + { + string[] validPaths = new string[validPathCount]; + for( int i = 0, j = 0; i < pathsSplit.Length; i++ ) + { + if( !string.IsNullOrEmpty( pathsSplit[i] ) ) + validPaths[j++] = pathsSplit[i]; + } + + pathsSplit = validPaths; + } + + result = pathsSplit; + } + + callbackHelper.CallOnMainThread( () => callbackMultiple( ( result != null && result.Length > 0 ) ? result : null ) ); + } + } +} +#endif \ No newline at end of file diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGMediaReceiveCallbackAndroid.cs.meta b/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGMediaReceiveCallbackAndroid.cs.meta new file mode 100644 index 0000000..ab75fb0 --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGMediaReceiveCallbackAndroid.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 4c18d702b07a63945968db47201b95c9 +timeCreated: 1519060539 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGPermissionCallbackAndroid.cs b/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGPermissionCallbackAndroid.cs new file mode 100644 index 0000000..71d6b41 --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGPermissionCallbackAndroid.cs @@ -0,0 +1,48 @@ +#if UNITY_EDITOR || UNITY_ANDROID +using System.Threading; +using UnityEngine; + +namespace NativeGalleryNamespace +{ + public class NGPermissionCallbackAndroid : AndroidJavaProxy + { + private object threadLock; + public int Result { get; private set; } + + public NGPermissionCallbackAndroid( object threadLock ) : base( "com.yasirkula.unity.NativeGalleryPermissionReceiver" ) + { + Result = -1; + this.threadLock = threadLock; + } + + [UnityEngine.Scripting.Preserve] + public void OnPermissionResult( int result ) + { + Result = result; + + lock( threadLock ) + { + Monitor.Pulse( threadLock ); + } + } + } + + public class NGPermissionCallbackAsyncAndroid : AndroidJavaProxy + { + private readonly NativeGallery.PermissionCallback callback; + private readonly NGCallbackHelper callbackHelper; + + public NGPermissionCallbackAsyncAndroid( NativeGallery.PermissionCallback callback ) : base( "com.yasirkula.unity.NativeGalleryPermissionReceiver" ) + { + this.callback = callback; + callbackHelper = new GameObject( "NGCallbackHelper" ).AddComponent(); + } + + [UnityEngine.Scripting.Preserve] + public void OnPermissionResult( int result ) + { + callbackHelper.CallOnMainThread( () => callback( (NativeGallery.Permission) result ) ); + } + } +} +#endif \ No newline at end of file diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGPermissionCallbackAndroid.cs.meta b/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGPermissionCallbackAndroid.cs.meta new file mode 100644 index 0000000..1030058 --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NGPermissionCallbackAndroid.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: a07afac614af1294d8e72a3c083be028 +timeCreated: 1519060539 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NativeGallery.aar b/TheStrongestSnail/Assets/Plugins/NativeGallery/Android/NativeGallery.aar new file mode 100644 index 0000000000000000000000000000000000000000..3cac09b2dcf26e7476e4f92fb53ef6336bfaa005 GIT binary patch literal 32446 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 File.ReadAllText( SAVE_PATH ) ); + else + m_instance = new Settings(); + } + catch( System.Exception e ) + { + Debug.LogException( e ); + m_instance = new Settings(); + } + } + + return m_instance; + } + } + + public void Save() + { + File.WriteAllText( SAVE_PATH, JsonUtility.ToJson( this, true ) ); + } + +#if UNITY_2018_3_OR_NEWER + [SettingsProvider] + public static SettingsProvider CreatePreferencesGUI() + { + return new SettingsProvider( "Project/yasirkula/Native Gallery", SettingsScope.Project ) + { + guiHandler = ( searchContext ) => PreferencesGUI(), + keywords = new System.Collections.Generic.HashSet() { "Native", "Gallery", "Android", "iOS" } + }; + } +#endif + +#if !UNITY_2018_3_OR_NEWER + [PreferenceItem( "Native Gallery" )] +#endif + public static void PreferencesGUI() + { + EditorGUI.BeginChangeCheck(); + + Instance.AutomatedSetup = EditorGUILayout.Toggle( "Automated Setup", Instance.AutomatedSetup ); + + EditorGUI.BeginDisabledGroup( !Instance.AutomatedSetup ); +#if !UNITY_2018_1_OR_NEWER + Instance.MinimumiOSTarget8OrAbove = EditorGUILayout.Toggle( "Deployment Target Is 8.0 Or Above", Instance.MinimumiOSTarget8OrAbove ); +#endif + Instance.PhotoLibraryUsageDescription = EditorGUILayout.DelayedTextField( "Photo Library Usage Description", Instance.PhotoLibraryUsageDescription ); + Instance.PhotoLibraryAdditionsUsageDescription = EditorGUILayout.DelayedTextField( "Photo Library Additions Usage Description", Instance.PhotoLibraryAdditionsUsageDescription ); + Instance.DontAskLimitedPhotosPermissionAutomaticallyOnIos14 = EditorGUILayout.Toggle( new GUIContent( "Don't Ask Limited Photos Permission Automatically", "See: https://mackuba.eu/2020/07/07/photo-library-changes-ios-14/. It's recommended to keep this setting enabled" ), Instance.DontAskLimitedPhotosPermissionAutomaticallyOnIos14 ); + EditorGUI.EndDisabledGroup(); + + if( EditorGUI.EndChangeCheck() ) + Instance.Save(); + } + } + + public class NGPostProcessBuild + { +#if UNITY_IOS + [PostProcessBuild( 1 )] + public static void OnPostprocessBuild( BuildTarget target, string buildPath ) + { + if( !Settings.Instance.AutomatedSetup ) + return; + + if( target == BuildTarget.iOS ) + { + string pbxProjectPath = PBXProject.GetPBXProjectPath( buildPath ); + string plistPath = Path.Combine( buildPath, "Info.plist" ); + + PBXProject pbxProject = new PBXProject(); + pbxProject.ReadFromFile( pbxProjectPath ); + +#if UNITY_2019_3_OR_NEWER + string targetGUID = pbxProject.GetUnityFrameworkTargetGuid(); +#else + string targetGUID = pbxProject.TargetGuidByName( PBXProject.GetUnityTargetName() ); +#endif + + // Minimum supported iOS version on Unity 2018.1 and later is 8.0 +#if !UNITY_2018_1_OR_NEWER + if( !Settings.Instance.MinimumiOSTarget8OrAbove ) + { + pbxProject.AddBuildProperty( targetGUID, "OTHER_LDFLAGS", "-weak_framework Photos" ); + pbxProject.AddBuildProperty( targetGUID, "OTHER_LDFLAGS", "-weak_framework PhotosUI" ); + pbxProject.AddBuildProperty( targetGUID, "OTHER_LDFLAGS", "-framework AssetsLibrary" ); + pbxProject.AddBuildProperty( targetGUID, "OTHER_LDFLAGS", "-framework MobileCoreServices" ); + pbxProject.AddBuildProperty( targetGUID, "OTHER_LDFLAGS", "-framework ImageIO" ); + } + else +#endif + { + pbxProject.AddBuildProperty( targetGUID, "OTHER_LDFLAGS", "-weak_framework PhotosUI" ); + pbxProject.AddBuildProperty( targetGUID, "OTHER_LDFLAGS", "-framework Photos" ); + pbxProject.AddBuildProperty( targetGUID, "OTHER_LDFLAGS", "-framework MobileCoreServices" ); + pbxProject.AddBuildProperty( targetGUID, "OTHER_LDFLAGS", "-framework ImageIO" ); + } + + pbxProject.RemoveFrameworkFromProject( targetGUID, "Photos.framework" ); + pbxProject.RemoveFrameworkFromProject( targetGUID, "PhotosUI.framework" ); + + File.WriteAllText( pbxProjectPath, pbxProject.WriteToString() ); + + PlistDocument plist = new PlistDocument(); + plist.ReadFromString( File.ReadAllText( plistPath ) ); + + PlistElementDict rootDict = plist.root; + if( !string.IsNullOrEmpty( Settings.Instance.PhotoLibraryUsageDescription ) ) + rootDict.SetString( "NSPhotoLibraryUsageDescription", Settings.Instance.PhotoLibraryUsageDescription ); + if( !string.IsNullOrEmpty( Settings.Instance.PhotoLibraryAdditionsUsageDescription ) ) + rootDict.SetString( "NSPhotoLibraryAddUsageDescription", Settings.Instance.PhotoLibraryAdditionsUsageDescription ); + if( Settings.Instance.DontAskLimitedPhotosPermissionAutomaticallyOnIos14 ) + rootDict.SetBoolean( "PHPhotoLibraryPreventAutomaticLimitedAccessAlert", true ); + + File.WriteAllText( plistPath, plist.WriteToString() ); + } + } +#endif + } +} \ No newline at end of file diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/Editor/NGPostProcessBuild.cs.meta b/TheStrongestSnail/Assets/Plugins/NativeGallery/Editor/NGPostProcessBuild.cs.meta new file mode 100644 index 0000000..bd4beff --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/Editor/NGPostProcessBuild.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: dff1540cf22bfb749a2422f445cf9427 +timeCreated: 1521452119 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/Editor/NativeGallery.Editor.asmdef b/TheStrongestSnail/Assets/Plugins/NativeGallery/Editor/NativeGallery.Editor.asmdef new file mode 100644 index 0000000..d129dd2 --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/Editor/NativeGallery.Editor.asmdef @@ -0,0 +1,15 @@ +{ + "name": "NativeGallery.Editor", + "references": [], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/Editor/NativeGallery.Editor.asmdef.meta b/TheStrongestSnail/Assets/Plugins/NativeGallery/Editor/NativeGallery.Editor.asmdef.meta new file mode 100644 index 0000000..4683509 --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/Editor/NativeGallery.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 3dffc8e654f00c545a82d0a5274d51eb +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/NativeGallery.Runtime.asmdef b/TheStrongestSnail/Assets/Plugins/NativeGallery/NativeGallery.Runtime.asmdef new file mode 100644 index 0000000..c1fb4a7 --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/NativeGallery.Runtime.asmdef @@ -0,0 +1,3 @@ +{ + "name": "NativeGallery.Runtime" +} diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/NativeGallery.Runtime.asmdef.meta b/TheStrongestSnail/Assets/Plugins/NativeGallery/NativeGallery.Runtime.asmdef.meta new file mode 100644 index 0000000..097882b --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/NativeGallery.Runtime.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 6e5063adab271564ba0098a06a8cebda +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/NativeGallery.cs b/TheStrongestSnail/Assets/Plugins/NativeGallery/NativeGallery.cs new file mode 100644 index 0000000..e080f97 --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/NativeGallery.cs @@ -0,0 +1,1039 @@ +using System; +using System.Globalization; +using System.IO; +using UnityEngine; +#if UNITY_2018_4_OR_NEWER && !NATIVE_GALLERY_DISABLE_ASYNC_FUNCTIONS +using System.Threading.Tasks; +using Unity.Collections; +using UnityEngine.Networking; +#endif +#if UNITY_ANDROID || UNITY_IOS +using NativeGalleryNamespace; +#endif +using Object = UnityEngine.Object; + +public static class NativeGallery +{ + public struct ImageProperties + { + public readonly int width; + public readonly int height; + public readonly string mimeType; + public readonly ImageOrientation orientation; + + public ImageProperties( int width, int height, string mimeType, ImageOrientation orientation ) + { + this.width = width; + this.height = height; + this.mimeType = mimeType; + this.orientation = orientation; + } + } + + public struct VideoProperties + { + public readonly int width; + public readonly int height; + public readonly long duration; + public readonly float rotation; + + public VideoProperties( int width, int height, long duration, float rotation ) + { + this.width = width; + this.height = height; + this.duration = duration; + this.rotation = rotation; + } + } + + public enum PermissionType { Read = 0, Write = 1 }; + public enum Permission { Denied = 0, Granted = 1, ShouldAsk = 2 }; + + [Flags] + public enum MediaType { Image = 1, Video = 2, Audio = 4 }; + + // EXIF orientation: http://sylvana.net/jpegcrop/exif_orientation.html (indices are reordered) + public enum ImageOrientation { Unknown = -1, Normal = 0, Rotate90 = 1, Rotate180 = 2, Rotate270 = 3, FlipHorizontal = 4, Transpose = 5, FlipVertical = 6, Transverse = 7 }; + + public delegate void PermissionCallback( Permission permission ); + public delegate void MediaSaveCallback( bool success, string path ); + public delegate void MediaPickCallback( string path ); + public delegate void MediaPickMultipleCallback( string[] paths ); + + #region Platform Specific Elements +#if !UNITY_EDITOR && UNITY_ANDROID + private static AndroidJavaClass m_ajc = null; + private static AndroidJavaClass AJC + { + get + { + if( m_ajc == null ) + m_ajc = new AndroidJavaClass( "com.yasirkula.unity.NativeGallery" ); + + return m_ajc; + } + } + + private static AndroidJavaObject m_context = null; + private static AndroidJavaObject Context + { + get + { + if( m_context == null ) + { + using( AndroidJavaObject unityClass = new AndroidJavaClass( "com.unity3d.player.UnityPlayer" ) ) + { + m_context = unityClass.GetStatic( "currentActivity" ); + } + } + + return m_context; + } + } +#elif !UNITY_EDITOR && UNITY_IOS + [System.Runtime.InteropServices.DllImport( "__Internal" )] + private static extern int _NativeGallery_CheckPermission( int readPermission, int permissionFreeMode ); + + [System.Runtime.InteropServices.DllImport( "__Internal" )] + private static extern int _NativeGallery_RequestPermission( int readPermission, int permissionFreeMode, int asyncMode ); + + [System.Runtime.InteropServices.DllImport( "__Internal" )] + private static extern void _NativeGallery_ShowLimitedLibraryPicker(); + + [System.Runtime.InteropServices.DllImport( "__Internal" )] + private static extern int _NativeGallery_CanOpenSettings(); + + [System.Runtime.InteropServices.DllImport( "__Internal" )] + private static extern void _NativeGallery_OpenSettings(); + + [System.Runtime.InteropServices.DllImport( "__Internal" )] + private static extern int _NativeGallery_CanPickMultipleMedia(); + + [System.Runtime.InteropServices.DllImport( "__Internal" )] + private static extern int _NativeGallery_GetMediaTypeFromExtension( string extension ); + + [System.Runtime.InteropServices.DllImport( "__Internal" )] + private static extern void _NativeGallery_ImageWriteToAlbum( string path, string album, int permissionFreeMode ); + + [System.Runtime.InteropServices.DllImport( "__Internal" )] + private static extern void _NativeGallery_VideoWriteToAlbum( string path, string album, int permissionFreeMode ); + + [System.Runtime.InteropServices.DllImport( "__Internal" )] + private static extern void _NativeGallery_PickMedia( string mediaSavePath, int mediaType, int permissionFreeMode, int selectionLimit ); + + [System.Runtime.InteropServices.DllImport( "__Internal" )] + private static extern string _NativeGallery_GetImageProperties( string path ); + + [System.Runtime.InteropServices.DllImport( "__Internal" )] + private static extern string _NativeGallery_GetVideoProperties( string path ); + + [System.Runtime.InteropServices.DllImport( "__Internal" )] + private static extern string _NativeGallery_GetVideoThumbnail( string path, string thumbnailSavePath, int maxSize, double captureTimeInSeconds ); + + [System.Runtime.InteropServices.DllImport( "__Internal" )] + private static extern string _NativeGallery_LoadImageAtPath( string path, string temporaryFilePath, int maxSize ); +#endif + +#if !UNITY_EDITOR && ( UNITY_ANDROID || UNITY_IOS ) + private static string m_temporaryImagePath = null; + private static string TemporaryImagePath + { + get + { + if( m_temporaryImagePath == null ) + { + m_temporaryImagePath = Path.Combine( Application.temporaryCachePath, "tmpImg" ); + Directory.CreateDirectory( Application.temporaryCachePath ); + } + + return m_temporaryImagePath; + } + } + + private static string m_selectedMediaPath = null; + private static string SelectedMediaPath + { + get + { + if( m_selectedMediaPath == null ) + { + m_selectedMediaPath = Path.Combine( Application.temporaryCachePath, "pickedMedia" ); + Directory.CreateDirectory( Application.temporaryCachePath ); + } + + return m_selectedMediaPath; + } + } +#endif + #endregion + + #region Runtime Permissions + // PermissionFreeMode was initially planned to be a toggleable setting on iOS but it has its own issues when set to false, so its value is forced to true. + // These issues are: + // - Presented permission dialog will have a "Select Photos" option on iOS 14+ but clicking it will freeze and eventually crash the app (I'm guessing that + // this is caused by how permissions are handled synchronously in NativeGallery) + // - While saving images/videos to Photos, iOS 14+ users would see the "Select Photos" option (which is irrelevant in this context, hence confusing) and + // the user must grant full Photos access in order to save the image/video to a custom album + // The only downside of having PermissionFreeMode = true is that, on iOS 14+, images/videos will be saved to the default Photos album rather than the + // provided custom album + private const bool PermissionFreeMode = true; + + public static Permission CheckPermission( PermissionType permissionType, MediaType mediaTypes ) + { +#if !UNITY_EDITOR && UNITY_ANDROID + Permission result = (Permission) AJC.CallStatic( "CheckPermission", Context, permissionType == PermissionType.Read, (int) mediaTypes ); + if( result == Permission.Denied && (Permission) PlayerPrefs.GetInt( "NativeGalleryPermission", (int) Permission.ShouldAsk ) == Permission.ShouldAsk ) + result = Permission.ShouldAsk; + + return result; +#elif !UNITY_EDITOR && UNITY_IOS + return ProcessPermission( (Permission) _NativeGallery_CheckPermission( permissionType == PermissionType.Read ? 1 : 0, PermissionFreeMode ? 1 : 0 ) ); +#else + return Permission.Granted; +#endif + } + + public static Permission RequestPermission( PermissionType permissionType, MediaType mediaTypes ) + { + // Don't block the main thread if the permission is already granted + if( CheckPermission( permissionType, mediaTypes ) == Permission.Granted ) + return Permission.Granted; + +#if !UNITY_EDITOR && UNITY_ANDROID + object threadLock = new object(); + lock( threadLock ) + { + NGPermissionCallbackAndroid nativeCallback = new NGPermissionCallbackAndroid( threadLock ); + + AJC.CallStatic( "RequestPermission", Context, nativeCallback, permissionType == PermissionType.Read, (int) mediaTypes, (int) Permission.ShouldAsk ); + + if( nativeCallback.Result == -1 ) + System.Threading.Monitor.Wait( threadLock ); + + if( (Permission) nativeCallback.Result != Permission.ShouldAsk && PlayerPrefs.GetInt( "NativeGalleryPermission", -1 ) != nativeCallback.Result ) + { + PlayerPrefs.SetInt( "NativeGalleryPermission", nativeCallback.Result ); + PlayerPrefs.Save(); + } + + return (Permission) nativeCallback.Result; + } +#elif !UNITY_EDITOR && UNITY_IOS + return ProcessPermission( (Permission) _NativeGallery_RequestPermission( permissionType == PermissionType.Read ? 1 : 0, PermissionFreeMode ? 1 : 0, 0 ) ); +#else + return Permission.Granted; +#endif + } + + public static void RequestPermissionAsync( PermissionCallback callback, PermissionType permissionType, MediaType mediaTypes ) + { +#if !UNITY_EDITOR && UNITY_ANDROID + NGPermissionCallbackAsyncAndroid nativeCallback = new NGPermissionCallbackAsyncAndroid( callback ); + AJC.CallStatic( "RequestPermission", Context, nativeCallback, permissionType == PermissionType.Read, (int) mediaTypes, (int) Permission.ShouldAsk ); +#elif !UNITY_EDITOR && UNITY_IOS + NGPermissionCallbackiOS.Initialize( ( result ) => callback( ProcessPermission( result ) ) ); + _NativeGallery_RequestPermission( permissionType == PermissionType.Read ? 1 : 0, PermissionFreeMode ? 1 : 0, 1 ); +#else + callback( Permission.Granted ); +#endif + } + +#if UNITY_2018_4_OR_NEWER && !NATIVE_GALLERY_DISABLE_ASYNC_FUNCTIONS + public static Task RequestPermissionAsync( PermissionType permissionType, MediaType mediaTypes ) + { + TaskCompletionSource tcs = new TaskCompletionSource(); + RequestPermissionAsync( ( permission ) => tcs.SetResult( permission ), permissionType, mediaTypes ); + return tcs.Task; + } +#endif + + private static Permission ProcessPermission( Permission permission ) + { + // result == 3: LimitedAccess permission on iOS, no need to handle it when PermissionFreeMode is set to true + return ( PermissionFreeMode && (int) permission == 3 ) ? Permission.Granted : permission; + } + + // This function isn't needed when PermissionFreeMode is set to true + private static void TryExtendLimitedAccessPermission() + { + if( IsMediaPickerBusy() ) + return; + +#if !UNITY_EDITOR && UNITY_IOS + _NativeGallery_ShowLimitedLibraryPicker(); +#endif + } + + public static bool CanOpenSettings() + { +#if !UNITY_EDITOR && UNITY_IOS + return _NativeGallery_CanOpenSettings() == 1; +#else + return true; +#endif + } + + public static void OpenSettings() + { +#if !UNITY_EDITOR && UNITY_ANDROID + AJC.CallStatic( "OpenSettings", Context ); +#elif !UNITY_EDITOR && UNITY_IOS + _NativeGallery_OpenSettings(); +#endif + } + #endregion + + #region Save Functions + public static Permission SaveImageToGallery( byte[] mediaBytes, string album, string filename, MediaSaveCallback callback = null ) + { + return SaveToGallery( mediaBytes, album, filename, MediaType.Image, callback ); + } + + public static Permission SaveImageToGallery( string existingMediaPath, string album, string filename, MediaSaveCallback callback = null ) + { + return SaveToGallery( existingMediaPath, album, filename, MediaType.Image, callback ); + } + + public static Permission SaveImageToGallery( Texture2D image, string album, string filename, MediaSaveCallback callback = null ) + { + if( image == null ) + throw new ArgumentException( "Parameter 'image' is null!" ); + + if( filename.EndsWith( ".jpeg", StringComparison.OrdinalIgnoreCase ) || filename.EndsWith( ".jpg", StringComparison.OrdinalIgnoreCase ) ) + return SaveToGallery( GetTextureBytes( image, true ), album, filename, MediaType.Image, callback ); + else if( filename.EndsWith( ".png", StringComparison.OrdinalIgnoreCase ) ) + return SaveToGallery( GetTextureBytes( image, false ), album, filename, MediaType.Image, callback ); + else + return SaveToGallery( GetTextureBytes( image, false ), album, filename + ".png", MediaType.Image, callback ); + } + + public static Permission SaveVideoToGallery( byte[] mediaBytes, string album, string filename, MediaSaveCallback callback = null ) + { + return SaveToGallery( mediaBytes, album, filename, MediaType.Video, callback ); + } + + public static Permission SaveVideoToGallery( string existingMediaPath, string album, string filename, MediaSaveCallback callback = null ) + { + return SaveToGallery( existingMediaPath, album, filename, MediaType.Video, callback ); + } + + private static Permission SaveAudioToGallery( byte[] mediaBytes, string album, string filename, MediaSaveCallback callback = null ) + { + return SaveToGallery( mediaBytes, album, filename, MediaType.Audio, callback ); + } + + private static Permission SaveAudioToGallery( string existingMediaPath, string album, string filename, MediaSaveCallback callback = null ) + { + return SaveToGallery( existingMediaPath, album, filename, MediaType.Audio, callback ); + } + #endregion + + #region Load Functions + public static bool CanSelectMultipleFilesFromGallery() + { +#if !UNITY_EDITOR && UNITY_ANDROID + return AJC.CallStatic( "CanSelectMultipleMedia" ); +#elif !UNITY_EDITOR && UNITY_IOS + return _NativeGallery_CanPickMultipleMedia() == 1; +#else + return false; +#endif + } + + public static bool CanSelectMultipleMediaTypesFromGallery() + { +#if UNITY_EDITOR + return true; +#elif UNITY_ANDROID + return AJC.CallStatic( "CanSelectMultipleMediaTypes" ); +#elif UNITY_IOS + return true; +#else + return false; +#endif + } + + public static Permission GetImageFromGallery( MediaPickCallback callback, string title = "", string mime = "image/*" ) + { + return GetMediaFromGallery( callback, MediaType.Image, mime, title ); + } + + public static Permission GetVideoFromGallery( MediaPickCallback callback, string title = "", string mime = "video/*" ) + { + return GetMediaFromGallery( callback, MediaType.Video, mime, title ); + } + + public static Permission GetAudioFromGallery( MediaPickCallback callback, string title = "", string mime = "audio/*" ) + { + return GetMediaFromGallery( callback, MediaType.Audio, mime, title ); + } + + public static Permission GetMixedMediaFromGallery( MediaPickCallback callback, MediaType mediaTypes, string title = "" ) + { + return GetMediaFromGallery( callback, mediaTypes, "*/*", title ); + } + + public static Permission GetImagesFromGallery( MediaPickMultipleCallback callback, string title = "", string mime = "image/*" ) + { + return GetMultipleMediaFromGallery( callback, MediaType.Image, mime, title ); + } + + public static Permission GetVideosFromGallery( MediaPickMultipleCallback callback, string title = "", string mime = "video/*" ) + { + return GetMultipleMediaFromGallery( callback, MediaType.Video, mime, title ); + } + + public static Permission GetAudiosFromGallery( MediaPickMultipleCallback callback, string title = "", string mime = "audio/*" ) + { + return GetMultipleMediaFromGallery( callback, MediaType.Audio, mime, title ); + } + + public static Permission GetMixedMediasFromGallery( MediaPickMultipleCallback callback, MediaType mediaTypes, string title = "" ) + { + return GetMultipleMediaFromGallery( callback, mediaTypes, "*/*", title ); + } + + public static bool IsMediaPickerBusy() + { +#if !UNITY_EDITOR && UNITY_IOS + return NGMediaReceiveCallbackiOS.IsBusy; +#else + return false; +#endif + } + + public static MediaType GetMediaTypeOfFile( string path ) + { + if( string.IsNullOrEmpty( path ) ) + return (MediaType) 0; + + string extension = Path.GetExtension( path ); + if( string.IsNullOrEmpty( extension ) ) + return (MediaType) 0; + + if( extension[0] == '.' ) + { + if( extension.Length == 1 ) + return (MediaType) 0; + + extension = extension.Substring( 1 ); + } + +#if UNITY_EDITOR + extension = extension.ToLowerInvariant(); + if( extension == "png" || extension == "jpg" || extension == "jpeg" || extension == "gif" || extension == "bmp" || extension == "tiff" ) + return MediaType.Image; + else if( extension == "mp4" || extension == "mov" || extension == "wav" || extension == "avi" ) + return MediaType.Video; + else if( extension == "mp3" || extension == "aac" || extension == "flac" ) + return MediaType.Audio; + + return (MediaType) 0; +#elif UNITY_ANDROID + string mime = AJC.CallStatic( "GetMimeTypeFromExtension", extension.ToLowerInvariant() ); + if( string.IsNullOrEmpty( mime ) ) + return (MediaType) 0; + else if( mime.StartsWith( "image/" ) ) + return MediaType.Image; + else if( mime.StartsWith( "video/" ) ) + return MediaType.Video; + else if( mime.StartsWith( "audio/" ) ) + return MediaType.Audio; + else + return (MediaType) 0; +#elif UNITY_IOS + return (MediaType) _NativeGallery_GetMediaTypeFromExtension( extension.ToLowerInvariant() ); +#else + return (MediaType) 0; +#endif + } + #endregion + + #region Internal Functions + private static Permission SaveToGallery( byte[] mediaBytes, string album, string filename, MediaType mediaType, MediaSaveCallback callback ) + { + Permission result = RequestPermission( PermissionType.Write, mediaType ); + if( result == Permission.Granted ) + { + if( mediaBytes == null || mediaBytes.Length == 0 ) + throw new ArgumentException( "Parameter 'mediaBytes' is null or empty!" ); + + if( album == null || album.Length == 0 ) + throw new ArgumentException( "Parameter 'album' is null or empty!" ); + + if( filename == null || filename.Length == 0 ) + throw new ArgumentException( "Parameter 'filename' is null or empty!" ); + + if( string.IsNullOrEmpty( Path.GetExtension( filename ) ) ) + Debug.LogWarning( "'filename' doesn't have an extension, this might result in unexpected behaviour!" ); + + string path = GetTemporarySavePath( filename ); +#if UNITY_EDITOR + Debug.Log( "SaveToGallery called successfully in the Editor" ); +#else + File.WriteAllBytes( path, mediaBytes ); +#endif + + SaveToGalleryInternal( path, album, mediaType, callback ); + } + + return result; + } + + private static Permission SaveToGallery( string existingMediaPath, string album, string filename, MediaType mediaType, MediaSaveCallback callback ) + { + Permission result = RequestPermission( PermissionType.Write, mediaType ); + if( result == Permission.Granted ) + { + if( !File.Exists( existingMediaPath ) ) + throw new FileNotFoundException( "File not found at " + existingMediaPath ); + + if( album == null || album.Length == 0 ) + throw new ArgumentException( "Parameter 'album' is null or empty!" ); + + if( filename == null || filename.Length == 0 ) + throw new ArgumentException( "Parameter 'filename' is null or empty!" ); + + if( string.IsNullOrEmpty( Path.GetExtension( filename ) ) ) + { + string originalExtension = Path.GetExtension( existingMediaPath ); + if( string.IsNullOrEmpty( originalExtension ) ) + Debug.LogWarning( "'filename' doesn't have an extension, this might result in unexpected behaviour!" ); + else + filename += originalExtension; + } + + string path = GetTemporarySavePath( filename ); +#if UNITY_EDITOR + Debug.Log( "SaveToGallery called successfully in the Editor" ); +#else + File.Copy( existingMediaPath, path, true ); +#endif + + SaveToGalleryInternal( path, album, mediaType, callback ); + } + + return result; + } + + private static void SaveToGalleryInternal( string path, string album, MediaType mediaType, MediaSaveCallback callback ) + { +#if !UNITY_EDITOR && UNITY_ANDROID + string savePath = AJC.CallStatic( "SaveMedia", Context, (int) mediaType, path, album ); + + File.Delete( path ); + + if( callback != null ) + callback( !string.IsNullOrEmpty( savePath ), savePath ); +#elif !UNITY_EDITOR && UNITY_IOS + if( mediaType == MediaType.Audio ) + { + Debug.LogError( "Saving audio files is not supported on iOS" ); + + if( callback != null ) + callback( false, null ); + + return; + } + + Debug.Log( "Saving to Pictures: " + Path.GetFileName( path ) ); + + NGMediaSaveCallbackiOS.Initialize( callback ); + if( mediaType == MediaType.Image ) + _NativeGallery_ImageWriteToAlbum( path, album, PermissionFreeMode ? 1 : 0 ); + else if( mediaType == MediaType.Video ) + _NativeGallery_VideoWriteToAlbum( path, album, PermissionFreeMode ? 1 : 0 ); +#else + if( callback != null ) + callback( true, null ); +#endif + } + + private static string GetTemporarySavePath( string filename ) + { + string saveDir = Path.Combine( Application.persistentDataPath, "NGallery" ); + Directory.CreateDirectory( saveDir ); + +#if !UNITY_EDITOR && UNITY_IOS + // Ensure a unique temporary filename on iOS: + // iOS internally copies images/videos to Photos directory of the system, + // but the process is async. The redundant file is deleted by objective-c code + // automatically after the media is saved but while it is being saved, the file + // should NOT be overwritten. Therefore, always ensure a unique filename on iOS + string path = Path.Combine( saveDir, filename ); + if( File.Exists( path ) ) + { + int fileIndex = 0; + string filenameWithoutExtension = Path.GetFileNameWithoutExtension( filename ); + string extension = Path.GetExtension( filename ); + + do + { + path = Path.Combine( saveDir, string.Concat( filenameWithoutExtension, ++fileIndex, extension ) ); + } while( File.Exists( path ) ); + } + + return path; +#else + return Path.Combine( saveDir, filename ); +#endif + } + + private static Permission GetMediaFromGallery( MediaPickCallback callback, MediaType mediaType, string mime, string title ) + { + Permission result = RequestPermission( PermissionType.Read, mediaType ); + if( result == Permission.Granted && !IsMediaPickerBusy() ) + { +#if UNITY_EDITOR + System.Collections.Generic.List editorFilters = new System.Collections.Generic.List( 4 ); + + if( ( mediaType & MediaType.Image ) == MediaType.Image ) + { + editorFilters.Add( "Image files" ); + editorFilters.Add( "png,jpg,jpeg" ); + } + + if( ( mediaType & MediaType.Video ) == MediaType.Video ) + { + editorFilters.Add( "Video files" ); + editorFilters.Add( "mp4,mov,webm,avi" ); + } + + if( ( mediaType & MediaType.Audio ) == MediaType.Audio ) + { + editorFilters.Add( "Audio files" ); + editorFilters.Add( "mp3,wav,aac,flac" ); + } + + editorFilters.Add( "All files" ); + editorFilters.Add( "*" ); + + string pickedFile = UnityEditor.EditorUtility.OpenFilePanelWithFilters( "Select file", "", editorFilters.ToArray() ); + + if( callback != null ) + callback( pickedFile != "" ? pickedFile : null ); +#elif UNITY_ANDROID + AJC.CallStatic( "PickMedia", Context, new NGMediaReceiveCallbackAndroid( callback, null ), (int) mediaType, false, SelectedMediaPath, mime, title ); +#elif UNITY_IOS + if( mediaType == MediaType.Audio ) + { + Debug.LogError( "Picking audio files is not supported on iOS" ); + + if( callback != null ) // Selecting audio files is not supported on iOS + callback( null ); + } + else + { + NGMediaReceiveCallbackiOS.Initialize( callback, null ); + _NativeGallery_PickMedia( SelectedMediaPath, (int) ( mediaType & ~MediaType.Audio ), PermissionFreeMode ? 1 : 0, 1 ); + } +#else + if( callback != null ) + callback( null ); +#endif + } + + return result; + } + + private static Permission GetMultipleMediaFromGallery( MediaPickMultipleCallback callback, MediaType mediaType, string mime, string title ) + { + Permission result = RequestPermission( PermissionType.Read, mediaType ); + if( result == Permission.Granted && !IsMediaPickerBusy() ) + { + if( CanSelectMultipleFilesFromGallery() ) + { +#if !UNITY_EDITOR && UNITY_ANDROID + AJC.CallStatic( "PickMedia", Context, new NGMediaReceiveCallbackAndroid( null, callback ), (int) mediaType, true, SelectedMediaPath, mime, title ); +#elif !UNITY_EDITOR && UNITY_IOS + if( mediaType == MediaType.Audio ) + { + Debug.LogError( "Picking audio files is not supported on iOS" ); + + if( callback != null ) // Selecting audio files is not supported on iOS + callback( null ); + } + else + { + NGMediaReceiveCallbackiOS.Initialize( null, callback ); + _NativeGallery_PickMedia( SelectedMediaPath, (int) ( mediaType & ~MediaType.Audio ), PermissionFreeMode ? 1 : 0, 0 ); + } +#else + if( callback != null ) + callback( null ); +#endif + } + else if( callback != null ) + callback( null ); + } + + return result; + } + + private static byte[] GetTextureBytes( Texture2D texture, bool isJpeg ) + { + try + { + return isJpeg ? texture.EncodeToJPG( 100 ) : texture.EncodeToPNG(); + } + catch( UnityException ) + { + return GetTextureBytesFromCopy( texture, isJpeg ); + } + catch( ArgumentException ) + { + return GetTextureBytesFromCopy( texture, isJpeg ); + } + +#pragma warning disable 0162 + return null; +#pragma warning restore 0162 + } + + private static byte[] GetTextureBytesFromCopy( Texture2D texture, bool isJpeg ) + { + // Texture is marked as non-readable, create a readable copy and save it instead + Debug.LogWarning( "Saving non-readable textures is slower than saving readable textures" ); + + Texture2D sourceTexReadable = null; + RenderTexture rt = RenderTexture.GetTemporary( texture.width, texture.height ); + RenderTexture activeRT = RenderTexture.active; + + try + { + Graphics.Blit( texture, rt ); + RenderTexture.active = rt; + + sourceTexReadable = new Texture2D( texture.width, texture.height, isJpeg ? TextureFormat.RGB24 : TextureFormat.RGBA32, false ); + sourceTexReadable.ReadPixels( new Rect( 0, 0, texture.width, texture.height ), 0, 0, false ); + sourceTexReadable.Apply( false, false ); + } + catch( Exception e ) + { + Debug.LogException( e ); + + Object.DestroyImmediate( sourceTexReadable ); + return null; + } + finally + { + RenderTexture.active = activeRT; + RenderTexture.ReleaseTemporary( rt ); + } + + try + { + return isJpeg ? sourceTexReadable.EncodeToJPG( 100 ) : sourceTexReadable.EncodeToPNG(); + } + catch( Exception e ) + { + Debug.LogException( e ); + return null; + } + finally + { + Object.DestroyImmediate( sourceTexReadable ); + } + } + +#if UNITY_2018_4_OR_NEWER && !NATIVE_GALLERY_DISABLE_ASYNC_FUNCTIONS + private static async Task TryCallNativeAndroidFunctionOnSeparateThread( Func function ) + { + T result = default( T ); + bool hasResult = false; + + await Task.Run( () => + { + if( AndroidJNI.AttachCurrentThread() != 0 ) + Debug.LogWarning( "Couldn't attach JNI thread, calling native function on the main thread" ); + else + { + try + { + result = function(); + hasResult = true; + } + finally + { + AndroidJNI.DetachCurrentThread(); + } + } + } ); + + return hasResult ? result : function(); + } +#endif + #endregion + + #region Utility Functions + public static Texture2D LoadImageAtPath( string imagePath, int maxSize = -1, bool markTextureNonReadable = true, bool generateMipmaps = true, bool linearColorSpace = false ) + { + if( string.IsNullOrEmpty( imagePath ) ) + throw new ArgumentException( "Parameter 'imagePath' is null or empty!" ); + + if( !File.Exists( imagePath ) ) + throw new FileNotFoundException( "File not found at " + imagePath ); + + if( maxSize <= 0 ) + maxSize = SystemInfo.maxTextureSize; + +#if !UNITY_EDITOR && UNITY_ANDROID + string loadPath = AJC.CallStatic( "LoadImageAtPath", Context, imagePath, TemporaryImagePath, maxSize ); +#elif !UNITY_EDITOR && UNITY_IOS + string loadPath = _NativeGallery_LoadImageAtPath( imagePath, TemporaryImagePath, maxSize ); +#else + string loadPath = imagePath; +#endif + + string extension = Path.GetExtension( imagePath ).ToLowerInvariant(); + TextureFormat format = ( extension == ".jpg" || extension == ".jpeg" ) ? TextureFormat.RGB24 : TextureFormat.RGBA32; + + Texture2D result = new Texture2D( 2, 2, format, generateMipmaps, linearColorSpace ); + + try + { + if( !result.LoadImage( File.ReadAllBytes( loadPath ), markTextureNonReadable ) ) + { + Debug.LogWarning( "Couldn't load image at path: " + loadPath ); + + Object.DestroyImmediate( result ); + return null; + } + } + catch( Exception e ) + { + Debug.LogException( e ); + + Object.DestroyImmediate( result ); + return null; + } + finally + { + if( loadPath != imagePath ) + { + try + { + File.Delete( loadPath ); + } + catch { } + } + } + + return result; + } + +#if UNITY_2018_4_OR_NEWER && !NATIVE_GALLERY_DISABLE_ASYNC_FUNCTIONS + public static async Task LoadImageAtPathAsync( string imagePath, int maxSize = -1, bool markTextureNonReadable = true ) + { + if( string.IsNullOrEmpty( imagePath ) ) + throw new ArgumentException( "Parameter 'imagePath' is null or empty!" ); + + if( !File.Exists( imagePath ) ) + throw new FileNotFoundException( "File not found at " + imagePath ); + + if( maxSize <= 0 ) + maxSize = SystemInfo.maxTextureSize; + +#if !UNITY_EDITOR && UNITY_ANDROID + string temporaryImagePath = TemporaryImagePath; // Must be accessed from main thread + string loadPath = await TryCallNativeAndroidFunctionOnSeparateThread( () => AJC.CallStatic( "LoadImageAtPath", Context, imagePath, temporaryImagePath, maxSize ) ); +#elif !UNITY_EDITOR && UNITY_IOS + string temporaryImagePath = TemporaryImagePath; // Must be accessed from main thread + string loadPath = await Task.Run( () => _NativeGallery_LoadImageAtPath( imagePath, temporaryImagePath, maxSize ) ); +#else + string loadPath = imagePath; +#endif + + Texture2D result = null; + + using( UnityWebRequest www = UnityWebRequestTexture.GetTexture( "file://" + loadPath, markTextureNonReadable ) ) + { + UnityWebRequestAsyncOperation asyncOperation = www.SendWebRequest(); + while( !asyncOperation.isDone ) + await Task.Yield(); + +#if UNITY_2020_1_OR_NEWER + if( www.result != UnityWebRequest.Result.Success ) +#else + if( www.isNetworkError || www.isHttpError ) +#endif + { + Debug.LogWarning( "Couldn't use UnityWebRequest to load image, falling back to LoadImage: " + www.error ); + } + else + result = DownloadHandlerTexture.GetContent( www ); + } + + if( !result ) // Fallback to Texture2D.LoadImage if something goes wrong + { + string extension = Path.GetExtension( imagePath ).ToLowerInvariant(); + TextureFormat format = ( extension == ".jpg" || extension == ".jpeg" ) ? TextureFormat.RGB24 : TextureFormat.RGBA32; + + result = new Texture2D( 2, 2, format, true, false ); + + try + { + if( !result.LoadImage( File.ReadAllBytes( loadPath ), markTextureNonReadable ) ) + { + Debug.LogWarning( "Couldn't load image at path: " + loadPath ); + + Object.DestroyImmediate( result ); + return null; + } + } + catch( Exception e ) + { + Debug.LogException( e ); + + Object.DestroyImmediate( result ); + return null; + } + finally + { + if( loadPath != imagePath ) + { + try + { + File.Delete( loadPath ); + } + catch { } + } + } + } + + return result; + } +#endif + + public static Texture2D GetVideoThumbnail( string videoPath, int maxSize = -1, double captureTimeInSeconds = -1.0, bool markTextureNonReadable = true, bool generateMipmaps = true, bool linearColorSpace = false ) + { + if( maxSize <= 0 ) + maxSize = SystemInfo.maxTextureSize; + +#if !UNITY_EDITOR && UNITY_ANDROID + string thumbnailPath = AJC.CallStatic( "GetVideoThumbnail", Context, videoPath, TemporaryImagePath + ".png", false, maxSize, captureTimeInSeconds ); +#elif !UNITY_EDITOR && UNITY_IOS + string thumbnailPath = _NativeGallery_GetVideoThumbnail( videoPath, TemporaryImagePath + ".png", maxSize, captureTimeInSeconds ); +#else + string thumbnailPath = null; +#endif + + if( !string.IsNullOrEmpty( thumbnailPath ) ) + return LoadImageAtPath( thumbnailPath, maxSize, markTextureNonReadable, generateMipmaps, linearColorSpace ); + else + return null; + } + +#if UNITY_2018_4_OR_NEWER && !NATIVE_GALLERY_DISABLE_ASYNC_FUNCTIONS + public static async Task GetVideoThumbnailAsync( string videoPath, int maxSize = -1, double captureTimeInSeconds = -1.0, bool markTextureNonReadable = true ) + { + if( maxSize <= 0 ) + maxSize = SystemInfo.maxTextureSize; + +#if !UNITY_EDITOR && UNITY_ANDROID + string temporaryImagePath = TemporaryImagePath; // Must be accessed from main thread + string thumbnailPath = await TryCallNativeAndroidFunctionOnSeparateThread( () => AJC.CallStatic( "GetVideoThumbnail", Context, videoPath, temporaryImagePath + ".png", false, maxSize, captureTimeInSeconds ) ); +#elif !UNITY_EDITOR && UNITY_IOS + string temporaryImagePath = TemporaryImagePath; // Must be accessed from main thread + string thumbnailPath = await Task.Run( () => _NativeGallery_GetVideoThumbnail( videoPath, temporaryImagePath + ".png", maxSize, captureTimeInSeconds ) ); +#else + string thumbnailPath = null; +#endif + + if( !string.IsNullOrEmpty( thumbnailPath ) ) + return await LoadImageAtPathAsync( thumbnailPath, maxSize, markTextureNonReadable ); + else + return null; + } +#endif + + public static ImageProperties GetImageProperties( string imagePath ) + { + if( !File.Exists( imagePath ) ) + throw new FileNotFoundException( "File not found at " + imagePath ); + +#if !UNITY_EDITOR && UNITY_ANDROID + string value = AJC.CallStatic( "GetImageProperties", Context, imagePath ); +#elif !UNITY_EDITOR && UNITY_IOS + string value = _NativeGallery_GetImageProperties( imagePath ); +#else + string value = null; +#endif + + int width = 0, height = 0; + string mimeType = null; + ImageOrientation orientation = ImageOrientation.Unknown; + if( !string.IsNullOrEmpty( value ) ) + { + string[] properties = value.Split( '>' ); + if( properties != null && properties.Length >= 4 ) + { + if( !int.TryParse( properties[0].Trim(), out width ) ) + width = 0; + if( !int.TryParse( properties[1].Trim(), out height ) ) + height = 0; + + mimeType = properties[2].Trim(); + if( mimeType.Length == 0 ) + { + string extension = Path.GetExtension( imagePath ).ToLowerInvariant(); + if( extension == ".png" ) + mimeType = "image/png"; + else if( extension == ".jpg" || extension == ".jpeg" ) + mimeType = "image/jpeg"; + else if( extension == ".gif" ) + mimeType = "image/gif"; + else if( extension == ".bmp" ) + mimeType = "image/bmp"; + else + mimeType = null; + } + + int orientationInt; + if( int.TryParse( properties[3].Trim(), out orientationInt ) ) + orientation = (ImageOrientation) orientationInt; + } + } + + return new ImageProperties( width, height, mimeType, orientation ); + } + + public static VideoProperties GetVideoProperties( string videoPath ) + { + if( !File.Exists( videoPath ) ) + throw new FileNotFoundException( "File not found at " + videoPath ); + +#if !UNITY_EDITOR && UNITY_ANDROID + string value = AJC.CallStatic( "GetVideoProperties", Context, videoPath ); +#elif !UNITY_EDITOR && UNITY_IOS + string value = _NativeGallery_GetVideoProperties( videoPath ); +#else + string value = null; +#endif + + int width = 0, height = 0; + long duration = 0L; + float rotation = 0f; + if( !string.IsNullOrEmpty( value ) ) + { + string[] properties = value.Split( '>' ); + if( properties != null && properties.Length >= 4 ) + { + if( !int.TryParse( properties[0].Trim(), out width ) ) + width = 0; + if( !int.TryParse( properties[1].Trim(), out height ) ) + height = 0; + if( !long.TryParse( properties[2].Trim(), out duration ) ) + duration = 0L; + if( !float.TryParse( properties[3].Trim().Replace( ',', '.' ), NumberStyles.Float, CultureInfo.InvariantCulture, out rotation ) ) + rotation = 0f; + } + } + + if( rotation == -90f ) + rotation = 270f; + + return new VideoProperties( width, height, duration, rotation ); + } + #endregion +} \ No newline at end of file diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/NativeGallery.cs.meta b/TheStrongestSnail/Assets/Plugins/NativeGallery/NativeGallery.cs.meta new file mode 100644 index 0000000..95d7159 --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/NativeGallery.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: ce1403606c3629046a0147d3e705f7cc +timeCreated: 1498722610 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/README.txt b/TheStrongestSnail/Assets/Plugins/NativeGallery/README.txt new file mode 100644 index 0000000..0dd4964 --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/README.txt @@ -0,0 +1,6 @@ += Native Gallery for Android & iOS (v1.8.1) = + +Documentation: https://github.com/yasirkula/UnityNativeGallery +FAQ: https://github.com/yasirkula/UnityNativeGallery#faq +Example code: https://github.com/yasirkula/UnityNativeGallery#example-code +E-mail: yasirkula@gmail.com \ No newline at end of file diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/README.txt.meta b/TheStrongestSnail/Assets/Plugins/NativeGallery/README.txt.meta new file mode 100644 index 0000000..4b9274d --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/README.txt.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: be769f45b807c40459e5bafb18e887d6 +timeCreated: 1563308465 +licenseType: Store +TextScriptImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS.meta b/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS.meta new file mode 100644 index 0000000..0474d23 --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 9c623599351a41a4c84c20f73c9d8976 +folderAsset: yes +timeCreated: 1498722622 +licenseType: Store +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGMediaReceiveCallbackiOS.cs b/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGMediaReceiveCallbackiOS.cs new file mode 100644 index 0000000..e30081f --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGMediaReceiveCallbackiOS.cs @@ -0,0 +1,132 @@ +#if UNITY_EDITOR || UNITY_IOS +using UnityEngine; + +namespace NativeGalleryNamespace +{ + public class NGMediaReceiveCallbackiOS : MonoBehaviour + { + private static NGMediaReceiveCallbackiOS instance; + + private NativeGallery.MediaPickCallback callback; + private NativeGallery.MediaPickMultipleCallback callbackMultiple; + + private float nextBusyCheckTime; + + public static bool IsBusy { get; private set; } + + [System.Runtime.InteropServices.DllImport( "__Internal" )] + private static extern int _NativeGallery_IsMediaPickerBusy(); + + public static void Initialize( NativeGallery.MediaPickCallback callback, NativeGallery.MediaPickMultipleCallback callbackMultiple ) + { + if( IsBusy ) + return; + + if( instance == null ) + { + instance = new GameObject( "NGMediaReceiveCallbackiOS" ).AddComponent(); + DontDestroyOnLoad( instance.gameObject ); + } + + instance.callback = callback; + instance.callbackMultiple = callbackMultiple; + + instance.nextBusyCheckTime = Time.realtimeSinceStartup + 1f; + IsBusy = true; + } + + private void Update() + { + if( IsBusy ) + { + if( Time.realtimeSinceStartup >= nextBusyCheckTime ) + { + nextBusyCheckTime = Time.realtimeSinceStartup + 1f; + + if( _NativeGallery_IsMediaPickerBusy() == 0 ) + { + IsBusy = false; + + NativeGallery.MediaPickCallback _callback = callback; + callback = null; + + NativeGallery.MediaPickMultipleCallback _callbackMultiple = callbackMultiple; + callbackMultiple = null; + + if( _callback != null ) + _callback( null ); + + if( _callbackMultiple != null ) + _callbackMultiple( null ); + } + } + } + } + + [UnityEngine.Scripting.Preserve] + public void OnMediaReceived( string path ) + { + IsBusy = false; + + if( string.IsNullOrEmpty( path ) ) + path = null; + + NativeGallery.MediaPickCallback _callback = callback; + callback = null; + + if( _callback != null ) + _callback( path ); + } + + [UnityEngine.Scripting.Preserve] + public void OnMultipleMediaReceived( string paths ) + { + IsBusy = false; + + string[] _paths = SplitPaths( paths ); + if( _paths != null && _paths.Length == 0 ) + _paths = null; + + NativeGallery.MediaPickMultipleCallback _callbackMultiple = callbackMultiple; + callbackMultiple = null; + + if( _callbackMultiple != null ) + _callbackMultiple( _paths ); + } + + private string[] SplitPaths( string paths ) + { + string[] result = null; + if( !string.IsNullOrEmpty( paths ) ) + { + string[] pathsSplit = paths.Split( '>' ); + + int validPathCount = 0; + for( int i = 0; i < pathsSplit.Length; i++ ) + { + if( !string.IsNullOrEmpty( pathsSplit[i] ) ) + validPathCount++; + } + + if( validPathCount == 0 ) + pathsSplit = new string[0]; + else if( validPathCount != pathsSplit.Length ) + { + string[] validPaths = new string[validPathCount]; + for( int i = 0, j = 0; i < pathsSplit.Length; i++ ) + { + if( !string.IsNullOrEmpty( pathsSplit[i] ) ) + validPaths[j++] = pathsSplit[i]; + } + + pathsSplit = validPaths; + } + + result = pathsSplit; + } + + return result; + } + } +} +#endif \ No newline at end of file diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGMediaReceiveCallbackiOS.cs.meta b/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGMediaReceiveCallbackiOS.cs.meta new file mode 100644 index 0000000..3ebbab4 --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGMediaReceiveCallbackiOS.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 71fb861c149c2d1428544c601e52a33c +timeCreated: 1519060539 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGMediaSaveCallbackiOS.cs b/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGMediaSaveCallbackiOS.cs new file mode 100644 index 0000000..222965b --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGMediaSaveCallbackiOS.cs @@ -0,0 +1,45 @@ +#if UNITY_EDITOR || UNITY_IOS +using UnityEngine; + +namespace NativeGalleryNamespace +{ + public class NGMediaSaveCallbackiOS : MonoBehaviour + { + private static NGMediaSaveCallbackiOS instance; + private NativeGallery.MediaSaveCallback callback; + + public static void Initialize( NativeGallery.MediaSaveCallback callback ) + { + if( instance == null ) + { + instance = new GameObject( "NGMediaSaveCallbackiOS" ).AddComponent(); + DontDestroyOnLoad( instance.gameObject ); + } + else if( instance.callback != null ) + instance.callback( false, null ); + + instance.callback = callback; + } + + [UnityEngine.Scripting.Preserve] + public void OnMediaSaveCompleted( string message ) + { + NativeGallery.MediaSaveCallback _callback = callback; + callback = null; + + if( _callback != null ) + _callback( true, null ); + } + + [UnityEngine.Scripting.Preserve] + public void OnMediaSaveFailed( string error ) + { + NativeGallery.MediaSaveCallback _callback = callback; + callback = null; + + if( _callback != null ) + _callback( false, null ); + } + } +} +#endif \ No newline at end of file diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGMediaSaveCallbackiOS.cs.meta b/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGMediaSaveCallbackiOS.cs.meta new file mode 100644 index 0000000..be840a4 --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGMediaSaveCallbackiOS.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 9cbb865d0913a0d47bb6d2eb3ad04c4f +timeCreated: 1519060539 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGPermissionCallbackiOS.cs b/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGPermissionCallbackiOS.cs new file mode 100644 index 0000000..1936f76 --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGPermissionCallbackiOS.cs @@ -0,0 +1,35 @@ +#if UNITY_EDITOR || UNITY_IOS +using UnityEngine; + +namespace NativeGalleryNamespace +{ + public class NGPermissionCallbackiOS : MonoBehaviour + { + private static NGPermissionCallbackiOS instance; + private NativeGallery.PermissionCallback callback; + + public static void Initialize( NativeGallery.PermissionCallback callback ) + { + if( instance == null ) + { + instance = new GameObject( "NGPermissionCallbackiOS" ).AddComponent(); + DontDestroyOnLoad( instance.gameObject ); + } + else if( instance.callback != null ) + instance.callback( NativeGallery.Permission.ShouldAsk ); + + instance.callback = callback; + } + + [UnityEngine.Scripting.Preserve] + public void OnPermissionRequested( string message ) + { + NativeGallery.PermissionCallback _callback = callback; + callback = null; + + if( _callback != null ) + _callback( (NativeGallery.Permission) int.Parse( message ) ); + } + } +} +#endif \ No newline at end of file diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGPermissionCallbackiOS.cs.meta b/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGPermissionCallbackiOS.cs.meta new file mode 100644 index 0000000..d62f57d --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NGPermissionCallbackiOS.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: bc6d7fa0a99114a45b1a6800097c6eb1 +timeCreated: 1519060539 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NativeGallery.mm b/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NativeGallery.mm new file mode 100644 index 0000000..8655160 --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NativeGallery.mm @@ -0,0 +1,1589 @@ +#import +#import +#import +#import +#import +#if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000 +#import +#endif +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 +#import +#endif + +#ifdef UNITY_4_0 || UNITY_5_0 +#import "iPhone_View.h" +#else +extern UIViewController* UnityGetGLViewController(); +#endif + +#define CHECK_IOS_VERSION( version ) ([[[UIDevice currentDevice] systemVersion] compare:version options:NSNumericSearch] != NSOrderedAscending) + +@interface UNativeGallery:NSObject ++ (int)checkPermission:(BOOL)readPermission permissionFreeMode:(BOOL)permissionFreeMode; ++ (int)requestPermission:(BOOL)readPermission permissionFreeMode:(BOOL)permissionFreeMode asyncMode:(BOOL)asyncMode; ++ (void)showLimitedLibraryPicker; ++ (int)canOpenSettings; ++ (void)openSettings; ++ (int)canPickMultipleMedia; ++ (void)saveMedia:(NSString *)path albumName:(NSString *)album isImg:(BOOL)isImg permissionFreeMode:(BOOL)permissionFreeMode; ++ (void)pickMedia:(int)mediaType savePath:(NSString *)mediaSavePath permissionFreeMode:(BOOL)permissionFreeMode selectionLimit:(int)selectionLimit; ++ (int)isMediaPickerBusy; ++ (int)getMediaTypeFromExtension:(NSString *)extension; ++ (char *)getImageProperties:(NSString *)path; ++ (char *)getVideoProperties:(NSString *)path; ++ (char *)getVideoThumbnail:(NSString *)path savePath:(NSString *)savePath maximumSize:(int)maximumSize captureTime:(double)captureTime; ++ (char *)loadImageAtPath:(NSString *)path tempFilePath:(NSString *)tempFilePath maximumSize:(int)maximumSize; +@end + +@implementation UNativeGallery + +static NSString *pickedMediaSavePath; +static UIPopoverController *popup; +static UIImagePickerController *imagePicker; +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 +static PHPickerViewController *imagePickerNew; +#endif +static int imagePickerState = 0; // 0 -> none, 1 -> showing (always in this state on iPad), 2 -> finished +static BOOL simpleMediaPickMode; +static BOOL pickingMultipleFiles = NO; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" ++ (int)checkPermission:(BOOL)readPermission permissionFreeMode:(BOOL)permissionFreeMode +{ +#if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000 + if( CHECK_IOS_VERSION( @"8.0" ) ) + { +#endif + // version >= iOS 8: check permission using Photos framework + + // On iOS 11 and later, permission isn't mandatory to fetch media from Photos + if( readPermission && permissionFreeMode && CHECK_IOS_VERSION( @"11.0" ) ) + return 1; + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 + // Photos permissions has changed on iOS 14 + if( CHECK_IOS_VERSION( @"14.0" ) ) + { + // Request ReadWrite permission in 2 cases: + // 1) When attempting to pick media from Photos with PHPhotoLibrary (readPermission=true and permissionFreeMode=false) + // 2) When attempting to write media to a specific album in Photos using PHPhotoLibrary (readPermission=false and permissionFreeMode=false) + PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatusForAccessLevel:( ( readPermission || !permissionFreeMode ) ? PHAccessLevelReadWrite : PHAccessLevelAddOnly )]; + if( status == PHAuthorizationStatusAuthorized ) + return 1; + else if( status == PHAuthorizationStatusRestricted ) + return 3; + else if( status == PHAuthorizationStatusNotDetermined ) + return 2; + else + return 0; + } + else +#endif + { + PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus]; + if( status == PHAuthorizationStatusAuthorized ) + return 1; + else if( status == PHAuthorizationStatusNotDetermined ) + return 2; + else + return 0; + } +#if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000 + } + else + { + // version < iOS 8: check permission using AssetsLibrary framework (Photos framework not available) + ALAuthorizationStatus status = [ALAssetsLibrary authorizationStatus]; + if( status == ALAuthorizationStatusAuthorized ) + return 1; + else if( status == ALAuthorizationStatusNotDetermined ) + return 2; + else + return 0; + } +#endif +} +#pragma clang diagnostic pop + ++ (int)requestPermission:(BOOL)readPermission permissionFreeMode:(BOOL)permissionFreeMode asyncMode:(BOOL)asyncMode +{ + int result; +#if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000 + if( CHECK_IOS_VERSION( @"8.0" ) ) + { +#endif + // version >= iOS 8: request permission using Photos framework + + // On iOS 11 and later, permission isn't mandatory to fetch media from Photos + if( readPermission && permissionFreeMode && CHECK_IOS_VERSION( @"11.0" ) ) + result = 1; +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 + else if( CHECK_IOS_VERSION( @"14.0" ) ) + { + // Photos permissions has changed on iOS 14. There are 2 permission dialogs now: + // - AddOnly permission dialog: has 2 options: "Allow" and "Don't Allow". This dialog grants permission for save operations only. Unfortunately, + // saving media to a custom album isn't possible with this dialog, media can only be saved to the default Photos album + // - ReadWrite permission dialog: has 3 options: "Allow Access to All Photos" (i.e. full permission), "Select Photos" (i.e. limited access) and + // "Don't Allow". To be able to save media to a custom album, user must grant Full Photos permission. Thus, even when readPermission is false, + // this dialog will be used if PermissionFreeMode is set to false. So, PermissionFreeMode determines whether or not saving to a custom album is + // be supported + result = [self requestPermissionNewest:( readPermission || !permissionFreeMode ) asyncMode:asyncMode]; + } +#endif + else + result = [self requestPermissionNew:asyncMode]; +#if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000 + } + else + { + // version < iOS 8: request permission using AssetsLibrary framework (Photos framework not available) + result = [self requestPermissionOld:asyncMode]; + } +#endif + + if( asyncMode && result >= 0 ) // Result returned immediately, forward it + UnitySendMessage( "NGPermissionCallbackiOS", "OnPermissionRequested", [self getCString:[NSString stringWithFormat:@"%d", result]] ); + + return result; +} + +#if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000 +// Credit: https://stackoverflow.com/a/26933380/2373034 +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" ++ (int)requestPermissionOld:(BOOL)asyncMode +{ + ALAuthorizationStatus status = [ALAssetsLibrary authorizationStatus]; + + if( status == ALAuthorizationStatusAuthorized ) + return 1; + else if( status == ALAuthorizationStatusNotDetermined ) + { + if( asyncMode ) + { + [[[ALAssetsLibrary alloc] init] enumerateGroupsWithTypes:ALAssetsGroupAll usingBlock:^( ALAssetsGroup *group, BOOL *stop ) + { + *stop = YES; + UnitySendMessage( "NGPermissionCallbackiOS", "OnPermissionRequested", "1" ); + } + failureBlock:^( NSError *error ) + { + UnitySendMessage( "NGPermissionCallbackiOS", "OnPermissionRequested", "0" ); + }]; + + return -1; + } + else + { + __block BOOL authorized = NO; + dispatch_semaphore_t sema = dispatch_semaphore_create( 0 ); + [[[ALAssetsLibrary alloc] init] enumerateGroupsWithTypes:ALAssetsGroupAll usingBlock:^( ALAssetsGroup *group, BOOL *stop ) + { + *stop = YES; + authorized = YES; + dispatch_semaphore_signal( sema ); + } + failureBlock:^( NSError *error ) + { + dispatch_semaphore_signal( sema ); + }]; + dispatch_semaphore_wait( sema, DISPATCH_TIME_FOREVER ); + + return authorized ? 1 : 0; + } + } + + return 0; +} +#pragma clang diagnostic pop +#endif + +// Credit: https://stackoverflow.com/a/32989022/2373034 ++ (int)requestPermissionNew:(BOOL)asyncMode +{ + PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus]; + + if( status == PHAuthorizationStatusAuthorized ) + return 1; + else if( status == PHAuthorizationStatusNotDetermined ) + { + if( asyncMode ) + { + [PHPhotoLibrary requestAuthorization:^( PHAuthorizationStatus status ) + { + UnitySendMessage( "NGPermissionCallbackiOS", "OnPermissionRequested", ( status == PHAuthorizationStatusAuthorized ) ? "1" : "0" ); + }]; + + return -1; + } + else + { + __block BOOL authorized = NO; + + dispatch_semaphore_t sema = dispatch_semaphore_create( 0 ); + [PHPhotoLibrary requestAuthorization:^( PHAuthorizationStatus status ) + { + authorized = ( status == PHAuthorizationStatusAuthorized ); + dispatch_semaphore_signal( sema ); + }]; + dispatch_semaphore_wait( sema, DISPATCH_TIME_FOREVER ); + + return authorized ? 1 : 0; + } + } + + return 0; +} + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 ++ (int)requestPermissionNewest:(BOOL)readPermission asyncMode:(BOOL)asyncMode +{ + PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatusForAccessLevel:( readPermission ? PHAccessLevelReadWrite : PHAccessLevelAddOnly )]; + + if( status == PHAuthorizationStatusAuthorized ) + return 1; + else if( status == PHAuthorizationStatusRestricted ) + return 3; + else if( status == PHAuthorizationStatusNotDetermined ) + { + if( asyncMode ) + { + [PHPhotoLibrary requestAuthorizationForAccessLevel:( readPermission ? PHAccessLevelReadWrite : PHAccessLevelAddOnly ) handler:^( PHAuthorizationStatus status ) + { + if( status == PHAuthorizationStatusAuthorized ) + UnitySendMessage( "NGPermissionCallbackiOS", "OnPermissionRequested", "1" ); + else if( status == PHAuthorizationStatusRestricted ) + UnitySendMessage( "NGPermissionCallbackiOS", "OnPermissionRequested", "3" ); + else + UnitySendMessage( "NGPermissionCallbackiOS", "OnPermissionRequested", "0" ); + }]; + + return -1; + } + else + { + __block int authorized = 0; + + dispatch_semaphore_t sema = dispatch_semaphore_create( 0 ); + [PHPhotoLibrary requestAuthorizationForAccessLevel:( readPermission ? PHAccessLevelReadWrite : PHAccessLevelAddOnly ) handler:^( PHAuthorizationStatus status ) + { + if( status == PHAuthorizationStatusAuthorized ) + authorized = 1; + else if( status == PHAuthorizationStatusRestricted ) + authorized = 3; + + dispatch_semaphore_signal( sema ); + }]; + dispatch_semaphore_wait( sema, DISPATCH_TIME_FOREVER ); + + return authorized; + } + } + + return 0; +} +#endif + +// When Photos permission is set to restricted, allows user to change the permission or change the list of restricted images +// It doesn't support a deterministic callback; for example there is a photoLibraryDidChange event but it won't be invoked if +// user doesn't change the list of restricted images ++ (void)showLimitedLibraryPicker +{ +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 + PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatusForAccessLevel:PHAccessLevelReadWrite]; + if( status == PHAuthorizationStatusNotDetermined ) + [self requestPermissionNewest:YES asyncMode:YES]; + else if( status == PHAuthorizationStatusRestricted ) + [[PHPhotoLibrary sharedPhotoLibrary] presentLimitedLibraryPickerFromViewController:UnityGetGLViewController()]; +#endif +} + +// Credit: https://stackoverflow.com/a/25453667/2373034 ++ (int)canOpenSettings +{ + return ( &UIApplicationOpenSettingsURLString != NULL ) ? 1 : 0; +} + +// Credit: https://stackoverflow.com/a/25453667/2373034 +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" ++ (void)openSettings +{ + if( &UIApplicationOpenSettingsURLString != NULL ) + { +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 100000 + if( CHECK_IOS_VERSION( @"10.0" ) ) + [[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString] options:@{} completionHandler:nil]; + else +#endif + [[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]]; + } +} +#pragma clang diagnostic pop + ++ (int)canPickMultipleMedia +{ +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 + if( CHECK_IOS_VERSION( @"14.0" ) ) + return 1; + else +#endif + return 0; +} + ++ (void)saveMedia:(NSString *)path albumName:(NSString *)album isImg:(BOOL)isImg permissionFreeMode:(BOOL)permissionFreeMode +{ +#if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000 + if( CHECK_IOS_VERSION( @"8.0" ) ) + { +#endif + // version >= iOS 8: save to specified album using Photos framework + // On iOS 14+, permission workflow has changed significantly with the addition of PHAuthorizationStatusRestricted permission. On those versions, + // user must grant Full Photos permission to be able to save to a custom album. Hence, there are 2 workflows: + // - If PermissionFreeMode is enabled, save the media directly to the default album (i.e. ignore 'album' parameter). This will present a simple + // permission dialog stating "The app requires access to Photos to save media to it." and the "Selected Photos" permission won't be listed in the options + // - Otherwise, the more complex "The app requires access to Photos to interact with it." permission dialog will be shown and if the user grants + // Full Photos permission, only then the image will be saved to the specified album. If user selects "Selected Photos" permission, default album will be + // used as fallback + [self saveMediaNew:path albumName:album isImage:isImg saveToDefaultAlbum:( permissionFreeMode && CHECK_IOS_VERSION( @"14.0" ) )]; +#if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000 + } + else + { + // version < iOS 8: save using AssetsLibrary framework (Photos framework not available) + [self saveMediaOld:path albumName:album isImage:isImg]; + } +#endif +} + +#if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000 +// Credit: https://stackoverflow.com/a/22056664/2373034 +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" ++ (void)saveMediaOld:(NSString *)path albumName:(NSString *)album isImage:(BOOL)isImage +{ + ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init]; + + if( !isImage && ![library videoAtPathIsCompatibleWithSavedPhotosAlbum:[NSURL fileURLWithPath:path]]) + { + NSLog( @"Error saving video: Video format is not compatible with Photos" ); + [[NSFileManager defaultManager] removeItemAtPath:path error:nil]; + UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" ); + return; + } + + void (^saveBlock)(ALAssetsGroup *assetCollection) = ^void( ALAssetsGroup *assetCollection ) + { + void (^saveResultBlock)(NSURL *assetURL, NSError *error) = ^void( NSURL *assetURL, NSError *error ) + { + [[NSFileManager defaultManager] removeItemAtPath:path error:nil]; + + if( error.code == 0 ) + { + [library assetForURL:assetURL resultBlock:^( ALAsset *asset ) + { + [assetCollection addAsset:asset]; + UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveCompleted", "" ); + } + failureBlock:^( NSError* error ) + { + NSLog( @"Error moving asset to album: %@", error ); + UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" ); + }]; + } + else + { + NSLog( @"Error creating asset: %@", error ); + UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" ); + } + }; + + if( !isImage ) + [library writeImageDataToSavedPhotosAlbum:[NSData dataWithContentsOfFile:path] metadata:nil completionBlock:saveResultBlock]; + else + [library writeVideoAtPathToSavedPhotosAlbum:[NSURL fileURLWithPath:path] completionBlock:saveResultBlock]; + }; + + __block BOOL albumFound = NO; + [library enumerateGroupsWithTypes:ALAssetsGroupAlbum usingBlock:^( ALAssetsGroup *group, BOOL *stop ) + { + if( [[group valueForProperty:ALAssetsGroupPropertyName] isEqualToString:album] ) + { + *stop = YES; + albumFound = YES; + saveBlock( group ); + } + else if( group == nil && albumFound==NO ) + { + // Album doesn't exist + [library addAssetsGroupAlbumWithName:album resultBlock:^( ALAssetsGroup *group ) + { + saveBlock( group ); + } + failureBlock:^( NSError *error ) + { + NSLog( @"Error creating album: %@", error ); + [[NSFileManager defaultManager] removeItemAtPath:path error:nil]; + UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" ); + }]; + } + } + failureBlock:^( NSError* error ) + { + NSLog( @"Error listing albums: %@", error ); + [[NSFileManager defaultManager] removeItemAtPath:path error:nil]; + UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" ); + }]; +} +#pragma clang diagnostic pop +#endif + +// Credit: https://stackoverflow.com/a/39909129/2373034 ++ (void)saveMediaNew:(NSString *)path albumName:(NSString *)album isImage:(BOOL)isImage saveToDefaultAlbum:(BOOL)saveToDefaultAlbum +{ + void (^saveToPhotosAlbum)() = ^void() + { + if( isImage ) + { + // Try preserving image metadata (essential for animated gif images) + [[PHPhotoLibrary sharedPhotoLibrary] performChanges: + ^{ + [PHAssetChangeRequest creationRequestForAssetFromImageAtFileURL:[NSURL fileURLWithPath:path]]; + } + completionHandler:^( BOOL success, NSError *error ) + { + if( success ) + { + [[NSFileManager defaultManager] removeItemAtPath:path error:nil]; + UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveCompleted", "" ); + } + else + { + NSLog( @"Error creating asset in default Photos album: %@", error ); + + UIImage *image = [UIImage imageWithContentsOfFile:path]; + if( image != nil ) + UIImageWriteToSavedPhotosAlbum( image, self, @selector(image:didFinishSavingWithError:contextInfo:), (__bridge_retained void *) path ); + else + { + NSLog( @"Couldn't create UIImage from file at path: %@", path ); + [[NSFileManager defaultManager] removeItemAtPath:path error:nil]; + UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" ); + } + } + }]; + } + else + { + if( UIVideoAtPathIsCompatibleWithSavedPhotosAlbum( path ) ) + UISaveVideoAtPathToSavedPhotosAlbum( path, self, @selector(video:didFinishSavingWithError:contextInfo:), (__bridge_retained void *) path ); + else + { + NSLog( @"Video at path isn't compatible with saved photos album: %@", path ); + [[NSFileManager defaultManager] removeItemAtPath:path error:nil]; + UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" ); + } + } + }; + + void (^saveBlock)(PHAssetCollection *assetCollection) = ^void( PHAssetCollection *assetCollection ) + { + [[PHPhotoLibrary sharedPhotoLibrary] performChanges: + ^{ + PHAssetChangeRequest *assetChangeRequest; + if( isImage ) + assetChangeRequest = [PHAssetChangeRequest creationRequestForAssetFromImageAtFileURL:[NSURL fileURLWithPath:path]]; + else + assetChangeRequest = [PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:[NSURL fileURLWithPath:path]]; + + PHAssetCollectionChangeRequest *assetCollectionChangeRequest = [PHAssetCollectionChangeRequest changeRequestForAssetCollection:assetCollection]; + [assetCollectionChangeRequest addAssets:@[[assetChangeRequest placeholderForCreatedAsset]]]; + + } + completionHandler:^( BOOL success, NSError *error ) + { + if( success ) + { + [[NSFileManager defaultManager] removeItemAtPath:path error:nil]; + UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveCompleted", "" ); + } + else + { + NSLog( @"Error creating asset: %@", error ); + saveToPhotosAlbum(); + } + }]; + }; + + if( saveToDefaultAlbum ) + saveToPhotosAlbum(); + else + { + PHFetchOptions *fetchOptions = [[PHFetchOptions alloc] init]; + fetchOptions.predicate = [NSPredicate predicateWithFormat:@"localizedTitle = %@", album]; + PHFetchResult *fetchResult = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeAlbum subtype:PHAssetCollectionSubtypeAny options:fetchOptions]; + if( fetchResult.count > 0 ) + saveBlock( fetchResult.firstObject); + else + { + __block PHObjectPlaceholder *albumPlaceholder; + [[PHPhotoLibrary sharedPhotoLibrary] performChanges: + ^{ + PHAssetCollectionChangeRequest *changeRequest = [PHAssetCollectionChangeRequest creationRequestForAssetCollectionWithTitle:album]; + albumPlaceholder = changeRequest.placeholderForCreatedAssetCollection; + } + completionHandler:^( BOOL success, NSError *error ) + { + if( success ) + { + PHFetchResult *fetchResult = [PHAssetCollection fetchAssetCollectionsWithLocalIdentifiers:@[albumPlaceholder.localIdentifier] options:nil]; + if( fetchResult.count > 0 ) + saveBlock( fetchResult.firstObject); + else + { + NSLog( @"Error creating album: Album placeholder not found" ); + saveToPhotosAlbum(); + } + } + else + { + NSLog( @"Error creating album: %@", error ); + saveToPhotosAlbum(); + } + }]; + } + } +} + ++ (void)image:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo +{ + NSString* path = (__bridge_transfer NSString *)(contextInfo); + [[NSFileManager defaultManager] removeItemAtPath:path error:nil]; + + if( error == nil ) + UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveCompleted", "" ); + else + { + NSLog( @"Error saving image with UIImageWriteToSavedPhotosAlbum: %@", error ); + UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" ); + } +} + ++ (void)video:(NSString *)videoPath didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo +{ + NSString* path = (__bridge_transfer NSString *)(contextInfo); + [[NSFileManager defaultManager] removeItemAtPath:path error:nil]; + + if( error == nil ) + UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveCompleted", "" ); + else + { + NSLog( @"Error saving video with UISaveVideoAtPathToSavedPhotosAlbum: %@", error ); + UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" ); + } +} + +// Credit: https://stackoverflow.com/a/10531752/2373034 ++ (void)pickMedia:(int)mediaType savePath:(NSString *)mediaSavePath permissionFreeMode:(BOOL)permissionFreeMode selectionLimit:(int)selectionLimit +{ + pickedMediaSavePath = mediaSavePath; + imagePickerState = 1; + simpleMediaPickMode = permissionFreeMode && CHECK_IOS_VERSION( @"11.0" ); + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 + if( CHECK_IOS_VERSION( @"14.0" ) ) + { + // PHPickerViewController is used on iOS 14 + PHPickerConfiguration *config = simpleMediaPickMode ? [[PHPickerConfiguration alloc] init] : [[PHPickerConfiguration alloc] initWithPhotoLibrary:[PHPhotoLibrary sharedPhotoLibrary]]; + config.preferredAssetRepresentationMode = PHPickerConfigurationAssetRepresentationModeCurrent; + config.selectionLimit = selectionLimit; + pickingMultipleFiles = selectionLimit != 1; + + // mediaType is a bitmask: + // 1: image + // 2: video + // 4: audio (not supported) + if( mediaType == 1 ) + config.filter = [PHPickerFilter anyFilterMatchingSubfilters:[NSArray arrayWithObjects:[PHPickerFilter imagesFilter], [PHPickerFilter livePhotosFilter], nil]]; + else if( mediaType == 2 ) + config.filter = [PHPickerFilter videosFilter]; + else + config.filter = [PHPickerFilter anyFilterMatchingSubfilters:[NSArray arrayWithObjects:[PHPickerFilter imagesFilter], [PHPickerFilter livePhotosFilter], [PHPickerFilter videosFilter], nil]]; + + imagePickerNew = [[PHPickerViewController alloc] initWithConfiguration:config]; + imagePickerNew.delegate = (id) self; + [UnityGetGLViewController() presentViewController:imagePickerNew animated:YES completion:^{ imagePickerState = 0; }]; + } + else +#endif + { + // UIImagePickerController is used on previous versions + imagePicker = [[UIImagePickerController alloc] init]; + imagePicker.delegate = (id) self; + imagePicker.allowsEditing = NO; + imagePicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; + + // mediaType is a bitmask: + // 1: image + // 2: video + // 4: audio (not supported) + if( mediaType == 1 ) + { + if( CHECK_IOS_VERSION( @"9.1" ) ) + imagePicker.mediaTypes = [NSArray arrayWithObjects:(NSString *)kUTTypeImage, (NSString *)kUTTypeLivePhoto, nil]; + else + imagePicker.mediaTypes = [NSArray arrayWithObject:(NSString *)kUTTypeImage]; + } + else if( mediaType == 2 ) + imagePicker.mediaTypes = [NSArray arrayWithObjects:(NSString *)kUTTypeMovie, (NSString *)kUTTypeVideo, nil]; + else + { + if( CHECK_IOS_VERSION( @"9.1" ) ) + imagePicker.mediaTypes = [NSArray arrayWithObjects:(NSString *)kUTTypeImage, (NSString *)kUTTypeLivePhoto, (NSString *)kUTTypeMovie, (NSString *)kUTTypeVideo, nil]; + else + imagePicker.mediaTypes = [NSArray arrayWithObjects:(NSString *)kUTTypeImage, (NSString *)kUTTypeMovie, (NSString *)kUTTypeVideo, nil]; + } + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 + if( mediaType != 1 ) + { + // Don't compress picked videos if possible + if( CHECK_IOS_VERSION( @"11.0" ) ) + imagePicker.videoExportPreset = AVAssetExportPresetPassthrough; + } +#endif + + UIViewController *rootViewController = UnityGetGLViewController(); + if( UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone ) // iPhone + [rootViewController presentViewController:imagePicker animated:YES completion:^{ imagePickerState = 0; }]; + else + { + // iPad + popup = [[UIPopoverController alloc] initWithContentViewController:imagePicker]; + popup.delegate = (id) self; + [popup presentPopoverFromRect:CGRectMake( rootViewController.view.frame.size.width / 2, rootViewController.view.frame.size.height / 2, 1, 1 ) inView:rootViewController.view permittedArrowDirections:0 animated:YES]; + } + } +} + ++ (int)isMediaPickerBusy +{ + if( imagePickerState == 2 ) + return 1; + + if( imagePicker != nil ) + { + if( imagePickerState == 1 || [imagePicker presentingViewController] == UnityGetGLViewController() ) + return 1; + else + { + imagePicker = nil; + return 0; + } + } +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 + else if( CHECK_IOS_VERSION( @"14.0" ) && imagePickerNew != nil ) + { + if( imagePickerState == 1 || [imagePickerNew presentingViewController] == UnityGetGLViewController() ) + return 1; + else + { + imagePickerNew = nil; + return 0; + } + } +#endif + else + return 0; +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" ++ (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info +{ + NSString *resultPath = nil; + + if( [info[UIImagePickerControllerMediaType] isEqualToString:(NSString *)kUTTypeImage] ) + { + NSLog( @"Picked an image" ); + + // On iOS 8.0 or later, try to obtain the raw data of the image (which allows picking gifs properly or preserving metadata) + if( CHECK_IOS_VERSION( @"8.0" ) ) + { + PHAsset *asset = nil; +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 + if( CHECK_IOS_VERSION( @"11.0" ) ) + { + // Try fetching the source image via UIImagePickerControllerImageURL + NSURL *mediaUrl = info[UIImagePickerControllerImageURL]; + if( mediaUrl != nil ) + { + NSString *imagePath = [mediaUrl path]; + if( imagePath != nil && [[NSFileManager defaultManager] fileExistsAtPath:imagePath] ) + { + NSError *error; + NSString *newPath = [pickedMediaSavePath stringByAppendingPathExtension:[imagePath pathExtension]]; + + if( ![[NSFileManager defaultManager] fileExistsAtPath:newPath] || [[NSFileManager defaultManager] removeItemAtPath:newPath error:&error] ) + { + if( [[NSFileManager defaultManager] copyItemAtPath:imagePath toPath:newPath error:&error] ) + { + resultPath = newPath; + NSLog( @"Copied source image from UIImagePickerControllerImageURL" ); + } + else + NSLog( @"Error copying image: %@", error ); + } + else + NSLog( @"Error deleting existing image: %@", error ); + } + } + + if( resultPath == nil ) + asset = info[UIImagePickerControllerPHAsset]; + } +#endif + + if( resultPath == nil && !simpleMediaPickMode ) + { + if( asset == nil ) + { + NSURL *mediaUrl = info[UIImagePickerControllerReferenceURL] ?: info[UIImagePickerControllerMediaURL]; + if( mediaUrl != nil ) + asset = [[PHAsset fetchAssetsWithALAssetURLs:[NSArray arrayWithObject:mediaUrl] options:nil] firstObject]; + } + + resultPath = [self trySavePHAsset:asset atIndex:1]; + } + } + + if( resultPath == nil ) + { + // Save image as PNG + UIImage *image = info[UIImagePickerControllerOriginalImage]; + if( image != nil ) + { + resultPath = [pickedMediaSavePath stringByAppendingPathExtension:@"png"]; + if( ![self saveImageAsPNG:image toPath:resultPath] ) + { + NSLog( @"Error creating PNG image" ); + resultPath = nil; + } + } + else + NSLog( @"Error fetching original image from picker" ); + } + } + else if( CHECK_IOS_VERSION( @"9.1" ) && [info[UIImagePickerControllerMediaType] isEqualToString:(NSString *)kUTTypeLivePhoto] ) + { + NSLog( @"Picked a live photo" ); + + // Save live photo as PNG + UIImage *image = info[UIImagePickerControllerOriginalImage]; + if( image != nil ) + { + resultPath = [pickedMediaSavePath stringByAppendingPathExtension:@"png"]; + if( ![self saveImageAsPNG:image toPath:resultPath] ) + { + NSLog( @"Error creating PNG image" ); + resultPath = nil; + } + } + else + NSLog( @"Error fetching live photo's still image from picker" ); + } + else + { + NSLog( @"Picked a video" ); + + NSURL *mediaUrl = info[UIImagePickerControllerMediaURL] ?: info[UIImagePickerControllerReferenceURL]; + if( mediaUrl != nil ) + { + resultPath = [mediaUrl path]; + + // On iOS 13, picked file becomes unreachable as soon as the UIImagePickerController disappears, + // in that case, copy the video to a temporary location + if( CHECK_IOS_VERSION( @"13.0" ) ) + { + NSError *error; + NSString *newPath = [pickedMediaSavePath stringByAppendingPathExtension:[resultPath pathExtension]]; + + if( ![[NSFileManager defaultManager] fileExistsAtPath:newPath] || [[NSFileManager defaultManager] removeItemAtPath:newPath error:&error] ) + { + if( [[NSFileManager defaultManager] copyItemAtPath:resultPath toPath:newPath error:&error] ) + resultPath = newPath; + else + { + NSLog( @"Error copying video: %@", error ); + resultPath = nil; + } + } + else + { + NSLog( @"Error deleting existing video: %@", error ); + resultPath = nil; + } + } + } + } + + popup = nil; + imagePicker = nil; + imagePickerState = 2; + UnitySendMessage( "NGMediaReceiveCallbackiOS", "OnMediaReceived", [self getCString:resultPath] ); + + [picker dismissViewControllerAnimated:NO completion:nil]; +} +#pragma clang diagnostic pop + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 +// Credit: https://ikyle.me/blog/2020/phpickerviewcontroller ++(void)picker:(PHPickerViewController *)picker didFinishPicking:(NSArray *)results +{ + imagePickerNew = nil; + imagePickerState = 2; + + [picker dismissViewControllerAnimated:NO completion:nil]; + + if( results != nil && [results count] > 0 ) + { + NSMutableArray *resultPaths = [NSMutableArray arrayWithCapacity:[results count]]; + NSLock *arrayLock = [[NSLock alloc] init]; + dispatch_group_t group = dispatch_group_create(); + + for( int i = 0; i < [results count]; i++ ) + { + PHPickerResult *result = results[i]; + NSItemProvider *itemProvider = result.itemProvider; + NSString *assetIdentifier = result.assetIdentifier; + __block NSString *resultPath = nil; + + int j = i + 1; + + //NSLog( @"result: %@", result ); + //NSLog( @"%@", result.assetIdentifier); + //NSLog( @"%@", result.itemProvider); + + if( [itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage] ) + { + NSLog( @"Picked an image" ); + + if( !simpleMediaPickMode && assetIdentifier != nil ) + { + PHAsset *asset = [[PHAsset fetchAssetsWithLocalIdentifiers:[NSArray arrayWithObject:assetIdentifier] options:nil] firstObject]; + resultPath = [self trySavePHAsset:asset atIndex:j]; + } + + if( resultPath != nil ) + { + [arrayLock lock]; + [resultPaths addObject:resultPath]; + [arrayLock unlock]; + } + else + { + dispatch_group_enter( group ); + + [itemProvider loadFileRepresentationForTypeIdentifier:(NSString *)kUTTypeImage completionHandler:^( NSURL *url, NSError *error ) + { + if( url != nil ) + { + // Copy the image to a temporary location because the returned image will be deleted by the OS after this callback is completed + resultPath = [url path]; + NSString *newPath = [[NSString stringWithFormat:@"%@%d", pickedMediaSavePath, j] stringByAppendingPathExtension:[resultPath pathExtension]]; + + if( ![[NSFileManager defaultManager] fileExistsAtPath:newPath] || [[NSFileManager defaultManager] removeItemAtPath:newPath error:&error] ) + { + if( [[NSFileManager defaultManager] copyItemAtPath:resultPath toPath:newPath error:&error]) + resultPath = newPath; + else + { + NSLog( @"Error copying image: %@", error ); + resultPath = nil; + } + } + else + { + NSLog( @"Error deleting existing image: %@", error ); + resultPath = nil; + } + } + else + NSLog( @"Error getting the picked image's path: %@", error ); + + if( resultPath != nil ) + { + [arrayLock lock]; + [resultPaths addObject:resultPath]; + [arrayLock unlock]; + } + else + { + if( [itemProvider canLoadObjectOfClass:[UIImage class]] ) + { + dispatch_group_enter( group ); + + [itemProvider loadObjectOfClass:[UIImage class] completionHandler:^( __kindof id object, NSError *error ) + { + if( object != nil && [object isKindOfClass:[UIImage class]] ) + { + resultPath = [[NSString stringWithFormat:@"%@%d", pickedMediaSavePath, j] stringByAppendingPathExtension:@"png"]; + if( ![self saveImageAsPNG:(UIImage *)object toPath:resultPath] ) + { + NSLog( @"Error creating PNG image" ); + resultPath = nil; + } + } + else + NSLog( @"Error generating UIImage from picked image: %@", error ); + + [arrayLock lock]; + [resultPaths addObject:( resultPath != nil ? resultPath : @"" )]; + [arrayLock unlock]; + + dispatch_group_leave( group ); + }]; + } + else + { + NSLog( @"Can't generate UIImage from picked image" ); + + [arrayLock lock]; + [resultPaths addObject:@""]; + [arrayLock unlock]; + } + } + + dispatch_group_leave( group ); + }]; + } + } + else if( CHECK_IOS_VERSION( @"9.1" ) && [itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeLivePhoto] ) + { + NSLog( @"Picked a live photo" ); + + if( [itemProvider canLoadObjectOfClass:[UIImage class]] ) + { + dispatch_group_enter( group ); + + [itemProvider loadObjectOfClass:[UIImage class] completionHandler:^( __kindof id object, NSError *error ) + { + if( object != nil && [object isKindOfClass:[UIImage class]] ) + { + resultPath = [[NSString stringWithFormat:@"%@%d", pickedMediaSavePath, j] stringByAppendingPathExtension:@"png"]; + if( ![self saveImageAsPNG:(UIImage *)object toPath:resultPath] ) + { + NSLog( @"Error creating PNG image" ); + resultPath = nil; + } + } + else + NSLog( @"Error generating UIImage from picked live photo: %@", error ); + + [arrayLock lock]; + [resultPaths addObject:( resultPath != nil ? resultPath : @"" )]; + [arrayLock unlock]; + + dispatch_group_leave( group ); + }]; + } + else if( [itemProvider canLoadObjectOfClass:[PHLivePhoto class]] ) + { + dispatch_group_enter( group ); + + [itemProvider loadObjectOfClass:[PHLivePhoto class] completionHandler:^( __kindof id object, NSError *error ) + { + if( object != nil && [object isKindOfClass:[PHLivePhoto class]] ) + { + // Extract image data from live photo + // Credit: https://stackoverflow.com/a/41341675/2373034 + NSArray* livePhotoResources = [PHAssetResource assetResourcesForLivePhoto:(PHLivePhoto *)object]; + + PHAssetResource *livePhotoImage = nil; + for( int k = 0; k < [livePhotoResources count]; k++ ) + { + if( livePhotoResources[k].type == PHAssetResourceTypePhoto ) + { + livePhotoImage = livePhotoResources[k]; + break; + } + } + + if( livePhotoImage == nil ) + { + NSLog( @"Error extracting image data from live photo" ); + + [arrayLock lock]; + [resultPaths addObject:@""]; + [arrayLock unlock]; + } + else + { + dispatch_group_enter( group ); + + NSString *originalFilename = livePhotoImage.originalFilename; + if( originalFilename == nil || [originalFilename length] == 0 ) + resultPath = [NSString stringWithFormat:@"%@%d", pickedMediaSavePath, j]; + else + resultPath = [[NSString stringWithFormat:@"%@%d", pickedMediaSavePath, j] stringByAppendingPathExtension:[originalFilename pathExtension]]; + + [[PHAssetResourceManager defaultManager] writeDataForAssetResource:livePhotoImage toFile:[NSURL fileURLWithPath:resultPath] options:nil completionHandler:^( NSError * _Nullable error2 ) + { + if( error2 != nil ) + { + NSLog( @"Error saving image data from live photo: %@", error2 ); + resultPath = nil; + } + + [arrayLock lock]; + [resultPaths addObject:( resultPath != nil ? resultPath : @"" )]; + [arrayLock unlock]; + + dispatch_group_leave( group ); + }]; + } + } + else + { + NSLog( @"Error generating PHLivePhoto from picked live photo: %@", error ); + + [arrayLock lock]; + [resultPaths addObject:@""]; + [arrayLock unlock]; + } + + dispatch_group_leave( group ); + }]; + } + else + { + NSLog( @"Can't convert picked live photo to still image" ); + + [arrayLock lock]; + [resultPaths addObject:@""]; + [arrayLock unlock]; + } + } + else if( [itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeMovie] || [itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeVideo] ) + { + NSLog( @"Picked a video" ); + + // Get the video file's path + dispatch_group_enter( group ); + + [itemProvider loadFileRepresentationForTypeIdentifier:([itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeMovie] ? (NSString *)kUTTypeMovie : (NSString *)kUTTypeVideo) completionHandler:^( NSURL *url, NSError *error ) + { + if( url != nil ) + { + // Copy the video to a temporary location because the returned video will be deleted by the OS after this callback is completed + resultPath = [url path]; + NSString *newPath = [[NSString stringWithFormat:@"%@%d", pickedMediaSavePath, j] stringByAppendingPathExtension:[resultPath pathExtension]]; + + if( ![[NSFileManager defaultManager] fileExistsAtPath:newPath] || [[NSFileManager defaultManager] removeItemAtPath:newPath error:&error] ) + { + if( [[NSFileManager defaultManager] copyItemAtPath:resultPath toPath:newPath error:&error]) + resultPath = newPath; + else + { + NSLog( @"Error copying video: %@", error ); + resultPath = nil; + } + } + else + { + NSLog( @"Error deleting existing video: %@", error ); + resultPath = nil; + } + } + else + NSLog( @"Error getting the picked video's path: %@", error ); + + [arrayLock lock]; + [resultPaths addObject:( resultPath != nil ? resultPath : @"" )]; + [arrayLock unlock]; + + dispatch_group_leave( group ); + }]; + } + else + { + // Unknown media type picked? + NSLog( @"Couldn't determine type of picked media: %@", itemProvider ); + + [arrayLock lock]; + [resultPaths addObject:@""]; + [arrayLock unlock]; + } + } + + dispatch_group_notify( group, dispatch_get_main_queue(), + ^{ + if( !pickingMultipleFiles ) + UnitySendMessage( "NGMediaReceiveCallbackiOS", "OnMediaReceived", [self getCString:resultPaths[0]] ); + else + UnitySendMessage( "NGMediaReceiveCallbackiOS", "OnMultipleMediaReceived", [self getCString:[resultPaths componentsJoinedByString:@">"]] ); + }); + } + else + { + NSLog( @"No media picked" ); + + if( !pickingMultipleFiles ) + UnitySendMessage( "NGMediaReceiveCallbackiOS", "OnMediaReceived", "" ); + else + UnitySendMessage( "NGMediaReceiveCallbackiOS", "OnMultipleMediaReceived", "" ); + } +} +#endif + ++ (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker +{ + NSLog( @"UIImagePickerController cancelled" ); + + popup = nil; + imagePicker = nil; + UnitySendMessage( "NGMediaReceiveCallbackiOS", "OnMediaReceived", "" ); + + [picker dismissViewControllerAnimated:NO completion:nil]; +} + ++ (void)popoverControllerDidDismissPopover:(UIPopoverController *)popoverController +{ + NSLog( @"UIPopoverController dismissed" ); + + popup = nil; + imagePicker = nil; + UnitySendMessage( "NGMediaReceiveCallbackiOS", "OnMediaReceived", "" ); +} + ++ (NSString *)trySavePHAsset:(PHAsset *)asset atIndex:(int)filenameIndex +{ + if( asset == nil ) + return nil; + + __block NSString *resultPath = nil; + + PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init]; + options.synchronous = YES; + options.version = PHImageRequestOptionsVersionCurrent; + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 + if( CHECK_IOS_VERSION( @"13.0" ) ) + { + [[PHImageManager defaultManager] requestImageDataAndOrientationForAsset:asset options:options resultHandler:^( NSData *imageData, NSString *dataUTI, CGImagePropertyOrientation orientation, NSDictionary *imageInfo ) + { + if( imageData != nil ) + resultPath = [self trySaveSourceImage:imageData withInfo:imageInfo atIndex:filenameIndex]; + else + NSLog( @"Couldn't fetch raw image data" ); + }]; + } + else +#endif + { + [[PHImageManager defaultManager] requestImageDataForAsset:asset options:options resultHandler:^( NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *imageInfo ) + { + if( imageData != nil ) + resultPath = [self trySaveSourceImage:imageData withInfo:imageInfo atIndex:filenameIndex]; + else + NSLog( @"Couldn't fetch raw image data" ); + }]; + } + + return resultPath; +} + ++ (NSString *)trySaveSourceImage:(NSData *)imageData withInfo:(NSDictionary *)info atIndex:(int)filenameIndex +{ + NSString *filePath = info[@"PHImageFileURLKey"]; + if( filePath != nil ) // filePath can actually be an NSURL, convert it to NSString + filePath = [NSString stringWithFormat:@"%@", filePath]; + + if( filePath == nil || [filePath length] == 0 ) + { + filePath = info[@"PHImageFileUTIKey"]; + if( filePath != nil ) + filePath = [NSString stringWithFormat:@"%@", filePath]; + } + + NSString *resultPath; + if( filePath == nil || [filePath length] == 0 ) + resultPath = [NSString stringWithFormat:@"%@%d", pickedMediaSavePath, filenameIndex]; + else + resultPath = [[NSString stringWithFormat:@"%@%d", pickedMediaSavePath, filenameIndex] stringByAppendingPathExtension:[filePath pathExtension]]; + + NSError *error; + if( ![[NSFileManager defaultManager] fileExistsAtPath:resultPath] || [[NSFileManager defaultManager] removeItemAtPath:resultPath error:&error] ) + { + if( ![imageData writeToFile:resultPath atomically:YES] ) + { + NSLog( @"Error copying source image to file" ); + resultPath = nil; + } + } + else + { + NSLog( @"Error deleting existing image: %@", error ); + resultPath = nil; + } + + return resultPath; +} + +// Credit: https://lists.apple.com/archives/cocoa-dev/2012/Jan/msg00052.html ++ (int)getMediaTypeFromExtension:(NSString *)extension +{ + CFStringRef fileUTI = UTTypeCreatePreferredIdentifierForTag( kUTTagClassFilenameExtension, (__bridge CFStringRef) extension, NULL ); + + // mediaType is a bitmask: + // 1: image + // 2: video + // 4: audio (not supported) + int result = 0; + if( UTTypeConformsTo( fileUTI, kUTTypeImage ) ) + result = 1; + else if( CHECK_IOS_VERSION( @"9.1" ) && UTTypeConformsTo( fileUTI, kUTTypeLivePhoto ) ) + result = 1; + else if( UTTypeConformsTo( fileUTI, kUTTypeMovie ) || UTTypeConformsTo( fileUTI, kUTTypeVideo ) ) + result = 2; + else if( UTTypeConformsTo( fileUTI, kUTTypeAudio ) ) + result = 4; + + CFRelease( fileUTI ); + + return result; +} + +// Credit: https://stackoverflow.com/a/4170099/2373034 ++ (NSArray *)getImageMetadata:(NSString *)path +{ + int width = 0; + int height = 0; + int orientation = -1; + + CGImageSourceRef imageSource = CGImageSourceCreateWithURL( (__bridge CFURLRef) [NSURL fileURLWithPath:path], nil ); + if( imageSource != nil ) + { + NSDictionary *options = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:NO] forKey:(__bridge NSString *)kCGImageSourceShouldCache]; + CFDictionaryRef imageProperties = CGImageSourceCopyPropertiesAtIndex( imageSource, 0, (__bridge CFDictionaryRef) options ); + CFRelease( imageSource ); + + CGFloat widthF = 0.0f, heightF = 0.0f; + if( imageProperties != nil ) + { + if( CFDictionaryContainsKey( imageProperties, kCGImagePropertyPixelWidth ) ) + CFNumberGetValue( (CFNumberRef) CFDictionaryGetValue( imageProperties, kCGImagePropertyPixelWidth ), kCFNumberCGFloatType, &widthF ); + + if( CFDictionaryContainsKey( imageProperties, kCGImagePropertyPixelHeight ) ) + CFNumberGetValue( (CFNumberRef) CFDictionaryGetValue( imageProperties, kCGImagePropertyPixelHeight ), kCFNumberCGFloatType, &heightF ); + + if( CFDictionaryContainsKey( imageProperties, kCGImagePropertyOrientation ) ) + { + CFNumberGetValue( (CFNumberRef) CFDictionaryGetValue( imageProperties, kCGImagePropertyOrientation ), kCFNumberIntType, &orientation ); + + if( orientation > 4 ) + { + // Landscape image + CGFloat temp = widthF; + widthF = heightF; + heightF = temp; + } + } + + CFRelease( imageProperties ); + } + + width = (int) roundf( widthF ); + height = (int) roundf( heightF ); + } + + return [[NSArray alloc] initWithObjects:[NSNumber numberWithInt:width], [NSNumber numberWithInt:height], [NSNumber numberWithInt:orientation], nil]; +} + ++ (char *)getImageProperties:(NSString *)path +{ + NSArray *metadata = [self getImageMetadata:path]; + + int orientationUnity; + int orientation = [metadata[2] intValue]; + + // To understand the magic numbers, see ImageOrientation enum in NativeGallery.cs + // and http://sylvana.net/jpegcrop/exif_orientation.html + if( orientation == 1 ) + orientationUnity = 0; + else if( orientation == 2 ) + orientationUnity = 4; + else if( orientation == 3 ) + orientationUnity = 2; + else if( orientation == 4 ) + orientationUnity = 6; + else if( orientation == 5 ) + orientationUnity = 5; + else if( orientation == 6 ) + orientationUnity = 1; + else if( orientation == 7 ) + orientationUnity = 7; + else if( orientation == 8 ) + orientationUnity = 3; + else + orientationUnity = -1; + + return [self getCString:[NSString stringWithFormat:@"%d>%d> >%d", [metadata[0] intValue], [metadata[1] intValue], orientationUnity]]; +} + ++ (char *)getVideoProperties:(NSString *)path +{ + CGSize size = CGSizeZero; + float rotation = 0; + long long duration = 0; + + AVURLAsset *asset = [AVURLAsset URLAssetWithURL:[NSURL fileURLWithPath:path] options:nil]; + if( asset != nil ) + { + duration = (long long) round( CMTimeGetSeconds( [asset duration] ) * 1000 ); + CGAffineTransform transform = [asset preferredTransform]; + NSArray* videoTracks = [asset tracksWithMediaType:AVMediaTypeVideo]; + if( videoTracks != nil && [videoTracks count] > 0 ) + { + size = [[videoTracks objectAtIndex:0] naturalSize]; + transform = [[videoTracks objectAtIndex:0] preferredTransform]; + } + + rotation = atan2( transform.b, transform.a ) * ( 180.0 / M_PI ); + } + + return [self getCString:[NSString stringWithFormat:@"%d>%d>%lld>%f", (int) roundf( size.width ), (int) roundf( size.height ), duration, rotation]]; +} + ++ (char *)getVideoThumbnail:(NSString *)path savePath:(NSString *)savePath maximumSize:(int)maximumSize captureTime:(double)captureTime +{ + AVAssetImageGenerator *thumbnailGenerator = [[AVAssetImageGenerator alloc] initWithAsset:[[AVURLAsset alloc] initWithURL:[NSURL fileURLWithPath:path] options:nil]]; + thumbnailGenerator.appliesPreferredTrackTransform = YES; + thumbnailGenerator.maximumSize = CGSizeMake( (CGFloat) maximumSize, (CGFloat) maximumSize ); + thumbnailGenerator.requestedTimeToleranceBefore = kCMTimeZero; + thumbnailGenerator.requestedTimeToleranceAfter = kCMTimeZero; + + if( captureTime < 0.0 ) + captureTime = 0.0; + else + { + AVURLAsset *asset = [AVURLAsset URLAssetWithURL:[NSURL fileURLWithPath:path] options:nil]; + if( asset != nil ) + { + double videoDuration = CMTimeGetSeconds( [asset duration] ); + if( videoDuration > 0.0 && captureTime >= videoDuration - 0.1 ) + { + if( captureTime > videoDuration ) + captureTime = videoDuration; + + thumbnailGenerator.requestedTimeToleranceBefore = CMTimeMakeWithSeconds( 1.0, 600 ); + } + } + } + + NSError *error = nil; + CGImageRef image = [thumbnailGenerator copyCGImageAtTime:CMTimeMakeWithSeconds( captureTime, 600 ) actualTime:nil error:&error]; + if( image == nil ) + { + if( error != nil ) + NSLog( @"Error generating video thumbnail: %@", error ); + else + NSLog( @"Error generating video thumbnail..." ); + + return [self getCString:@""]; + } + + UIImage *thumbnail = [[UIImage alloc] initWithCGImage:image]; + CGImageRelease( image ); + + if( ![UIImagePNGRepresentation( thumbnail ) writeToFile:savePath atomically:YES] ) + { + NSLog( @"Error saving thumbnail image" ); + return [self getCString:@""]; + } + + return [self getCString:savePath]; +} + ++ (BOOL)saveImageAsPNG:(UIImage *)image toPath:(NSString *)resultPath +{ + return [UIImagePNGRepresentation( [self scaleImage:image maxSize:16384] ) writeToFile:resultPath atomically:YES]; +} + ++ (UIImage *)scaleImage:(UIImage *)image maxSize:(int)maxSize +{ + CGFloat width = image.size.width; + CGFloat height = image.size.height; + + UIImageOrientation orientation = image.imageOrientation; + if( width <= maxSize && height <= maxSize && orientation != UIImageOrientationDown && + orientation != UIImageOrientationLeft && orientation != UIImageOrientationRight && + orientation != UIImageOrientationLeftMirrored && orientation != UIImageOrientationRightMirrored && + orientation != UIImageOrientationUpMirrored && orientation != UIImageOrientationDownMirrored ) + return image; + + CGFloat scaleX = 1.0f; + CGFloat scaleY = 1.0f; + if( width > maxSize ) + scaleX = maxSize / width; + if( height > maxSize ) + scaleY = maxSize / height; + + // Credit: https://github.com/mbcharbonneau/UIImage-Categories/blob/master/UIImage%2BAlpha.m + CGImageAlphaInfo alpha = CGImageGetAlphaInfo( image.CGImage ); + BOOL hasAlpha = alpha == kCGImageAlphaFirst || alpha == kCGImageAlphaLast || alpha == kCGImageAlphaPremultipliedFirst || alpha == kCGImageAlphaPremultipliedLast; + + CGFloat scaleRatio = scaleX < scaleY ? scaleX : scaleY; + CGRect imageRect = CGRectMake( 0, 0, width * scaleRatio, height * scaleRatio ); + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 100000 + // Resize image with UIGraphicsImageRenderer (Apple's recommended API) if possible + if( CHECK_IOS_VERSION( @"10.0" ) ) + { + UIGraphicsImageRendererFormat *format = [image imageRendererFormat]; + format.opaque = !hasAlpha; + format.scale = image.scale; + + UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:imageRect.size format:format]; + image = [renderer imageWithActions:^( UIGraphicsImageRendererContext* _Nonnull myContext ) + { + [image drawInRect:imageRect]; + }]; + } + else + #endif + { + UIGraphicsBeginImageContextWithOptions( imageRect.size, !hasAlpha, image.scale ); + [image drawInRect:imageRect]; + image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + } + + return image; +} + ++ (char *)loadImageAtPath:(NSString *)path tempFilePath:(NSString *)tempFilePath maximumSize:(int)maximumSize +{ + // Check if the image can be loaded by Unity without requiring a conversion to PNG + // Credit: https://stackoverflow.com/a/12048937/2373034 + NSString *extension = [path pathExtension]; + BOOL conversionNeeded = [extension caseInsensitiveCompare:@"jpg"] != NSOrderedSame && [extension caseInsensitiveCompare:@"jpeg"] != NSOrderedSame && [extension caseInsensitiveCompare:@"png"] != NSOrderedSame; + + if( !conversionNeeded ) + { + // Check if the image needs to be processed at all + NSArray *metadata = [self getImageMetadata:path]; + int orientationInt = [metadata[2] intValue]; // 1: correct orientation, [1,8]: valid orientation range + if( orientationInt == 1 && [metadata[0] intValue] <= maximumSize && [metadata[1] intValue] <= maximumSize ) + return [self getCString:path]; + } + + UIImage *image = [UIImage imageWithContentsOfFile:path]; + if( image == nil ) + return [self getCString:path]; + + UIImage *scaledImage = [self scaleImage:image maxSize:maximumSize]; + if( conversionNeeded || scaledImage != image ) + { + if( ![UIImagePNGRepresentation( scaledImage ) writeToFile:tempFilePath atomically:YES] ) + { + NSLog( @"Error creating scaled image" ); + return [self getCString:path]; + } + + return [self getCString:tempFilePath]; + } + else + return [self getCString:path]; +} + +// Credit: https://stackoverflow.com/a/37052118/2373034 ++ (char *)getCString:(NSString *)source +{ + if( source == nil ) + source = @""; + + const char *sourceUTF8 = [source UTF8String]; + char *result = (char*) malloc( strlen( sourceUTF8 ) + 1 ); + strcpy( result, sourceUTF8 ); + + return result; +} + +@end + +extern "C" int _NativeGallery_CheckPermission( int readPermission, int permissionFreeMode ) +{ + return [UNativeGallery checkPermission:( readPermission == 1 ) permissionFreeMode:( permissionFreeMode == 1 )]; +} + +extern "C" int _NativeGallery_RequestPermission( int readPermission, int permissionFreeMode, int asyncMode ) +{ + return [UNativeGallery requestPermission:( readPermission == 1 ) permissionFreeMode:( permissionFreeMode == 1 ) asyncMode:( asyncMode == 1 )]; +} + +extern "C" void _NativeGallery_ShowLimitedLibraryPicker() +{ + return [UNativeGallery showLimitedLibraryPicker]; +} + +extern "C" int _NativeGallery_CanOpenSettings() +{ + return [UNativeGallery canOpenSettings]; +} + +extern "C" void _NativeGallery_OpenSettings() +{ + [UNativeGallery openSettings]; +} + +extern "C" int _NativeGallery_CanPickMultipleMedia() +{ + return [UNativeGallery canPickMultipleMedia]; +} + +extern "C" void _NativeGallery_ImageWriteToAlbum( const char* path, const char* album, int permissionFreeMode ) +{ + [UNativeGallery saveMedia:[NSString stringWithUTF8String:path] albumName:[NSString stringWithUTF8String:album] isImg:YES permissionFreeMode:( permissionFreeMode == 1 )]; +} + +extern "C" void _NativeGallery_VideoWriteToAlbum( const char* path, const char* album, int permissionFreeMode ) +{ + [UNativeGallery saveMedia:[NSString stringWithUTF8String:path] albumName:[NSString stringWithUTF8String:album] isImg:NO permissionFreeMode:( permissionFreeMode == 1 )]; +} + +extern "C" void _NativeGallery_PickMedia( const char* mediaSavePath, int mediaType, int permissionFreeMode, int selectionLimit ) +{ + [UNativeGallery pickMedia:mediaType savePath:[NSString stringWithUTF8String:mediaSavePath] permissionFreeMode:( permissionFreeMode == 1 ) selectionLimit:selectionLimit]; +} + +extern "C" int _NativeGallery_IsMediaPickerBusy() +{ + return [UNativeGallery isMediaPickerBusy]; +} + +extern "C" int _NativeGallery_GetMediaTypeFromExtension( const char* extension ) +{ + return [UNativeGallery getMediaTypeFromExtension:[NSString stringWithUTF8String:extension]]; +} + +extern "C" char* _NativeGallery_GetImageProperties( const char* path ) +{ + return [UNativeGallery getImageProperties:[NSString stringWithUTF8String:path]]; +} + +extern "C" char* _NativeGallery_GetVideoProperties( const char* path ) +{ + return [UNativeGallery getVideoProperties:[NSString stringWithUTF8String:path]]; +} + +extern "C" char* _NativeGallery_GetVideoThumbnail( const char* path, const char* thumbnailSavePath, int maxSize, double captureTimeInSeconds ) +{ + return [UNativeGallery getVideoThumbnail:[NSString stringWithUTF8String:path] savePath:[NSString stringWithUTF8String:thumbnailSavePath] maximumSize:maxSize captureTime:captureTimeInSeconds]; +} + +extern "C" char* _NativeGallery_LoadImageAtPath( const char* path, const char* temporaryFilePath, int maxSize ) +{ + return [UNativeGallery loadImageAtPath:[NSString stringWithUTF8String:path] tempFilePath:[NSString stringWithUTF8String:temporaryFilePath] maximumSize:maxSize]; +} \ No newline at end of file diff --git a/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NativeGallery.mm.meta b/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NativeGallery.mm.meta new file mode 100644 index 0000000..1823903 --- /dev/null +++ b/TheStrongestSnail/Assets/Plugins/NativeGallery/iOS/NativeGallery.mm.meta @@ -0,0 +1,33 @@ +fileFormatVersion: 2 +guid: 953e0b740eb03144883db35f72cad8a6 +timeCreated: 1498722774 +licenseType: Store +PluginImporter: + serializedVersion: 2 + iconMap: {} + executionOrder: {} + isPreloaded: 0 + isOverridable: 0 + platformData: + data: + first: + Any: + second: + enabled: 0 + settings: {} + data: + first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + data: + first: + iPhone: iOS + second: + enabled: 1 + settings: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/TheStrongestSnail/Assets/Scripts/LqUiScripts/EditPanel.cs b/TheStrongestSnail/Assets/Scripts/LqUiScripts/EditPanel.cs index 86b312a..bf29870 100644 --- a/TheStrongestSnail/Assets/Scripts/LqUiScripts/EditPanel.cs +++ b/TheStrongestSnail/Assets/Scripts/LqUiScripts/EditPanel.cs @@ -12,6 +12,8 @@ public class EditPanel : Base public InputField UserInputField; public Text username; public PerSonalCenterPanel perSonalCenterPanel; + + public Button avatar; // Start is called before the first frame update async void Start() { @@ -26,8 +28,50 @@ public class EditPanel : Base userInfomation14 = JsonConvert.DeserializeObject(response14); username.text = "" + userInfomation14.data.nickName; + avatar.onClick.AddListener(selectavatar); } + void selectavatar() + { + PickImage(512); + } + private void PickImage(int maxSize) + { + NativeGallery.Permission permission = NativeGallery.GetImageFromGallery((path) => + { + Debug.Log("Image path: " + path); + if (path != null) + { + // Create Texture from selected image + Texture2D texture = NativeGallery.LoadImageAtPath(path, maxSize); + if (texture == null) + { + Debug.Log("Couldn't load texture from " + path); + return; + } + + // Assign texture to a temporary quad and destroy it after 5 seconds + GameObject quad = GameObject.CreatePrimitive(PrimitiveType.Quad); + quad.transform.position = Camera.main.transform.position + Camera.main.transform.forward * 2.5f; + quad.transform.forward = Camera.main.transform.forward; + quad.transform.localScale = new Vector3(1f, texture.height / (float)texture.width, 1f); + + Material material = quad.GetComponent().material; + if (!material.shader.isSupported) // happens when Standard shader is not included in the build + material.shader = Shader.Find("Legacy Shaders/Diffuse"); + + material.mainTexture = texture; + + Destroy(quad, 5f); + + // If a procedural texture is not destroyed manually, + // it will only be freed after a scene change + Destroy(texture, 5f); + } + }); + + Debug.Log("Permission result: " + permission); + } // Update is called once per frame void Update() { diff --git a/TheStrongestSnail/Assets/prefabs/HYLPrefabs/PersonalCenterPanel.prefab b/TheStrongestSnail/Assets/prefabs/HYLPrefabs/PersonalCenterPanel.prefab index 2a2ca47..4632a2b 100644 --- a/TheStrongestSnail/Assets/prefabs/HYLPrefabs/PersonalCenterPanel.prefab +++ b/TheStrongestSnail/Assets/prefabs/HYLPrefabs/PersonalCenterPanel.prefab @@ -2069,7 +2069,6 @@ GameObject: m_Component: - component: {fileID: 6661102261700243289} - component: {fileID: 3692270011235859773} - - component: {fileID: 1821891998228383241} - component: {fileID: 4775368350903003862} m_Layer: 5 m_Name: PersonalCenterPanel @@ -2112,36 +2111,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 3661722200123750880} m_CullTransparentMesh: 1 ---- !u!114 &1821891998228383241 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 3661722200123750880} - m_Enabled: 0 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} - m_Name: - m_EditorClassIdentifier: - m_Material: {fileID: 0} - m_Color: {r: 1, g: 1, b: 1, a: 0} - m_RaycastTarget: 1 - m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} - m_Maskable: 1 - m_OnCullStateChanged: - m_PersistentCalls: - m_Calls: [] - m_Sprite: {fileID: 21300000, guid: c47381d146953bf419405b6a9a2f3f34, type: 3} - m_Type: 0 - m_PreserveAspect: 0 - m_FillCenter: 1 - m_FillMethod: 4 - m_FillAmount: 1 - m_FillClockwise: 1 - m_FillOrigin: 0 - m_UseSpriteMesh: 0 - m_PixelsPerUnitMultiplier: 1 --- !u!114 &4775368350903003862 MonoBehaviour: m_ObjectHideFlags: 0 diff --git a/TheStrongestSnail/Assets/prefabs/ge_ren_zhong_xing/EditPanel.prefab b/TheStrongestSnail/Assets/prefabs/ge_ren_zhong_xing/EditPanel.prefab index 3aa9e3a..f849966 100644 --- a/TheStrongestSnail/Assets/prefabs/ge_ren_zhong_xing/EditPanel.prefab +++ b/TheStrongestSnail/Assets/prefabs/ge_ren_zhong_xing/EditPanel.prefab @@ -468,6 +468,7 @@ MonoBehaviour: UserInputField: {fileID: 379801035829787383} username: {fileID: 8468291165135306074} perSonalCenterPanel: {fileID: 0} + avatar: {fileID: 2339787961763677450} --- !u!1 &6960989442929760420 GameObject: m_ObjectHideFlags: 0 @@ -752,6 +753,7 @@ GameObject: - component: {fileID: 8996637367871962009} - component: {fileID: 8996637367871962011} - component: {fileID: 8996637367871962010} + - component: {fileID: 2339787961763677450} m_Layer: 5 m_Name: Image m_TagString: Untagged @@ -817,6 +819,50 @@ MonoBehaviour: m_FillOrigin: 0 m_UseSpriteMesh: 0 m_PixelsPerUnitMultiplier: 1 +--- !u!114 &2339787961763677450 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8996637367871962008} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 8996637367871962010} + m_OnClick: + m_PersistentCalls: + m_Calls: [] --- !u!1 &8996637367919174697 GameObject: m_ObjectHideFlags: 0