This commit is contained in:
2020-07-18 21:12:14 +08:00
parent 1361db18a9
commit 33dc6cea60
214 changed files with 16596 additions and 3104 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 880df38a865ba4f91981f0af6cafd817
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,31 @@
#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 )
{
System.Action temp = mainThreadAction;
mainThreadAction = null;
temp();
}
}
public void CallOnMainThread( System.Action function )
{
mainThreadAction = function;
}
}
}
#endif

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 2d517fd0f2f85f24698df2775bee58e9
timeCreated: 1544889149
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,92 @@
#if !UNITY_EDITOR && UNITY_ANDROID
using UnityEngine;
namespace NativeGalleryNamespace
{
public class NGMediaReceiveCallbackAndroid : AndroidJavaProxy
{
private readonly NativeGallery.MediaPickCallback callback;
private readonly NativeGallery.MediaPickMultipleCallback callbackMultiple;
private readonly NGCallbackHelper callbackHelper;
public NGMediaReceiveCallbackAndroid( NativeGallery.MediaPickCallback callback, NativeGallery.MediaPickMultipleCallback callbackMultiple ) : base( "com.yasirkula.unity.NativeGalleryMediaReceiver" )
{
this.callback = callback;
this.callbackMultiple = callbackMultiple;
callbackHelper = new GameObject( "NGCallbackHelper" ).AddComponent<NGCallbackHelper>();
}
public void OnMediaReceived( string path )
{
callbackHelper.CallOnMainThread( () => MediaReceiveCallback( path ) );
}
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( () => MediaReceiveMultipleCallback( result ) );
}
private void MediaReceiveCallback( string path )
{
if( string.IsNullOrEmpty( path ) )
path = null;
try
{
if( callback != null )
callback( path );
}
finally
{
Object.Destroy( callbackHelper );
}
}
private void MediaReceiveMultipleCallback( string[] paths )
{
if( paths != null && paths.Length == 0 )
paths = null;
try
{
if( callbackMultiple != null )
callbackMultiple( paths );
}
finally
{
Object.Destroy( callbackHelper );
}
}
}
}
#endif

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 4c18d702b07a63945968db47201b95c9
timeCreated: 1519060539
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,29 @@
#if !UNITY_EDITOR && UNITY_ANDROID
using System.Threading;
using UnityEngine;
namespace NativeGalleryNamespace
{
public class NGPermissionCallbackAndroid : AndroidJavaProxy
{
private object threadLock;
public int Result { get; private set; }
public NGPermissionCallbackAndroid( object threadLock ) : base( "com.yasirkula.unity.NativeGalleryPermissionReceiver" )
{
Result = -1;
this.threadLock = threadLock;
}
public void OnPermissionResult( int result )
{
Result = result;
lock( threadLock )
{
Monitor.Pulse( threadLock );
}
}
}
}
#endif

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: a07afac614af1294d8e72a3c083be028
timeCreated: 1519060539
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,33 @@
fileFormatVersion: 2
guid: 8ed2b1cd685da484194ba437e57d1e49
timeCreated: 1570374101
licenseType: Free
PluginImporter:
serializedVersion: 2
iconMap: {}
executionOrder: {}
isPreloaded: 0
isOverridable: 0
platformData:
data:
first:
Android: Android
second:
enabled: 1
settings: {}
data:
first:
Any:
second:
enabled: 0
settings: {}
data:
first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d8aa34e33afa341a180dea50b2bdad62
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,72 @@
#if UNITY_IOS
using UnityEditor;
using UnityEditor.Callbacks;
using System.IO;
using UnityEditor.iOS.Xcode;
#endif
public class NGPostProcessBuild
{
private const bool ENABLED = true;
private const string PHOTO_LIBRARY_USAGE_DESCRIPTION = "Save media to Photos";
private const bool MINIMUM_TARGET_8_OR_ABOVE = false;
#if UNITY_IOS
#pragma warning disable 0162
[PostProcessBuild]
public static void OnPostprocessBuild( BuildTarget target, string buildPath )
{
if( !ENABLED )
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( MINIMUM_TARGET_8_OR_ABOVE )
{
#endif
pbxProject.AddBuildProperty( targetGUID, "OTHER_LDFLAGS", "-framework Photos" );
pbxProject.AddBuildProperty( targetGUID, "OTHER_LDFLAGS", "-framework MobileCoreServices" );
pbxProject.AddBuildProperty( targetGUID, "OTHER_LDFLAGS", "-framework ImageIO" );
#if !UNITY_2018_1_OR_NEWER
}
else
{
pbxProject.AddBuildProperty( targetGUID, "OTHER_LDFLAGS", "-weak_framework Photos" );
pbxProject.AddBuildProperty( targetGUID, "OTHER_LDFLAGS", "-framework AssetsLibrary" );
pbxProject.AddBuildProperty( targetGUID, "OTHER_LDFLAGS", "-framework MobileCoreServices" );
pbxProject.AddBuildProperty( targetGUID, "OTHER_LDFLAGS", "-framework ImageIO" );
}
#endif
pbxProject.RemoveFrameworkFromProject( targetGUID, "Photos.framework" );
File.WriteAllText( pbxProjectPath, pbxProject.WriteToString() );
PlistDocument plist = new PlistDocument();
plist.ReadFromString( File.ReadAllText( plistPath ) );
PlistElementDict rootDict = plist.root;
rootDict.SetString( "NSPhotoLibraryUsageDescription", PHOTO_LIBRARY_USAGE_DESCRIPTION );
rootDict.SetString( "NSPhotoLibraryAddUsageDescription", PHOTO_LIBRARY_USAGE_DESCRIPTION );
File.WriteAllText( plistPath, plist.WriteToString() );
}
}
#pragma warning restore 0162
#endif
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: dff1540cf22bfb749a2422f445cf9427
timeCreated: 1521452119
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,15 @@
{
"name": "NativeGallery.Editor",
"references": [],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,762 @@
using System;
using System.IO;
using UnityEngine;
using Object = UnityEngine.Object;
#if !UNITY_EDITOR && ( UNITY_ANDROID || UNITY_IOS )
using NativeGalleryNamespace;
#endif
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 Permission { Denied = 0, Granted = 1, ShouldAsk = 2 };
private enum MediaType { Image = 0, Video = 1, Audio = 2 };
// 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 MediaSaveCallback( string error );
public delegate void MediaPickCallback( string path );
public delegate void MediaPickMultipleCallback( string[] paths );
#region Platform Specific Elements
#if !UNITY_EDITOR && UNITY_ANDROID
private static AndroidJavaClass m_ajc = null;
private static AndroidJavaClass AJC
{
get
{
if( m_ajc == null )
m_ajc = new AndroidJavaClass( "com.yasirkula.unity.NativeGallery" );
return m_ajc;
}
}
private static AndroidJavaObject m_context = null;
private static AndroidJavaObject Context
{
get
{
if( m_context == null )
{
using( AndroidJavaObject unityClass = new AndroidJavaClass( "com.unity3d.player.UnityPlayer" ) )
{
m_context = unityClass.GetStatic<AndroidJavaObject>( "currentActivity" );
}
}
return m_context;
}
}
#elif !UNITY_EDITOR && UNITY_IOS
[System.Runtime.InteropServices.DllImport( "__Internal" )]
private static extern int _NativeGallery_CheckPermission();
[System.Runtime.InteropServices.DllImport( "__Internal" )]
private static extern int _NativeGallery_RequestPermission();
[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 void _NativeGallery_ImageWriteToAlbum( string path, string album );
[System.Runtime.InteropServices.DllImport( "__Internal" )]
private static extern void _NativeGallery_VideoWriteToAlbum( string path, string album );
[System.Runtime.InteropServices.DllImport( "__Internal" )]
private static extern void _NativeGallery_PickImage( string imageSavePath );
[System.Runtime.InteropServices.DllImport( "__Internal" )]
private static extern void _NativeGallery_PickVideo( string videoSavePath );
[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_selectedImagePath = null;
private static string SelectedImagePath
{
get
{
if( m_selectedImagePath == null )
{
m_selectedImagePath = Path.Combine( Application.temporaryCachePath, "pickedImg" );
Directory.CreateDirectory( Application.temporaryCachePath );
}
return m_selectedImagePath;
}
}
private static string m_selectedVideoPath = null;
private static string SelectedVideoPath
{
get
{
if( m_selectedVideoPath == null )
{
m_selectedVideoPath = Path.Combine( Application.temporaryCachePath, "pickedVideo" );
Directory.CreateDirectory( Application.temporaryCachePath );
}
return m_selectedVideoPath;
}
}
private static string m_selectedAudioPath = null;
private static string SelectedAudioPath
{
get
{
if( m_selectedAudioPath == null )
{
m_selectedAudioPath = Path.Combine( Application.temporaryCachePath, "pickedAudio" );
Directory.CreateDirectory( Application.temporaryCachePath );
}
return m_selectedAudioPath;
}
}
#endif
#endregion
#region Runtime Permissions
public static Permission CheckPermission( bool readPermissionOnly = false )
{
#if !UNITY_EDITOR && UNITY_ANDROID
Permission result = (Permission) AJC.CallStatic<int>( "CheckPermission", Context, readPermissionOnly );
if( result == Permission.Denied && (Permission) PlayerPrefs.GetInt( "NativeGalleryPermission", (int) Permission.ShouldAsk ) == Permission.ShouldAsk )
result = Permission.ShouldAsk;
return result;
#elif !UNITY_EDITOR && UNITY_IOS
return (Permission) _NativeGallery_CheckPermission();
#else
return Permission.Granted;
#endif
}
public static Permission RequestPermission( bool readPermissionOnly = false )
{
#if !UNITY_EDITOR && UNITY_ANDROID
object threadLock = new object();
lock( threadLock )
{
NGPermissionCallbackAndroid nativeCallback = new NGPermissionCallbackAndroid( threadLock );
AJC.CallStatic( "RequestPermission", Context, nativeCallback, readPermissionOnly, PlayerPrefs.GetInt( "NativeGalleryPermission", (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 (Permission) _NativeGallery_RequestPermission();
#else
return Permission.Granted;
#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" ) || filename.EndsWith( ".jpg" ) )
return SaveToGallery( GetTextureBytes( image, true ), album, filename, MediaType.Image, callback );
else if( filename.EndsWith( ".png" ) )
return SaveToGallery( GetTextureBytes( image, false ), album, filename, MediaType.Image, callback );
else
return SaveToGallery( GetTextureBytes( image, false ), album, filename + ".png", MediaType.Image, callback );
}
public static Permission SaveVideoToGallery( byte[] mediaBytes, string album, string filename, MediaSaveCallback callback = null )
{
return SaveToGallery( mediaBytes, album, filename, MediaType.Video, callback );
}
public static Permission SaveVideoToGallery( string existingMediaPath, string album, string filename, MediaSaveCallback callback = null )
{
return SaveToGallery( existingMediaPath, album, filename, MediaType.Video, callback );
}
private static Permission SaveAudioToGallery( byte[] mediaBytes, string album, string filename, MediaSaveCallback callback = null )
{
return SaveToGallery( mediaBytes, album, filename, MediaType.Audio, callback );
}
private static Permission SaveAudioToGallery( string existingMediaPath, string album, string filename, MediaSaveCallback callback = null )
{
return SaveToGallery( existingMediaPath, album, filename, MediaType.Audio, callback );
}
#endregion
#region Load Functions
public static bool CanSelectMultipleFilesFromGallery()
{
#if !UNITY_EDITOR && UNITY_ANDROID
return AJC.CallStatic<bool>( "CanSelectMultipleMedia" );
#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 );
}
private static Permission GetAudioFromGallery( MediaPickCallback callback, string title = "", string mime = "audio/*" )
{
return GetMediaFromGallery( callback, MediaType.Audio, mime, 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 );
}
private static Permission GetAudiosFromGallery( MediaPickMultipleCallback callback, string title = "", string mime = "audio/*" )
{
return GetMultipleMediaFromGallery( callback, MediaType.Audio, mime, title );
}
public static bool IsMediaPickerBusy()
{
#if !UNITY_EDITOR && UNITY_IOS
return NGMediaReceiveCallbackiOS.IsBusy;
#else
return false;
#endif
}
#endregion
#region Internal Functions
private static Permission SaveToGallery( byte[] mediaBytes, string album, string filename, MediaType mediaType, MediaSaveCallback callback )
{
Permission result = RequestPermission( false );
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( false );
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
AJC.CallStatic( "SaveMedia", Context, (int) mediaType, path, album );
File.Delete( path );
if( callback != null )
callback( null );
#elif !UNITY_EDITOR && UNITY_IOS
if( mediaType == MediaType.Audio )
{
if( callback != null )
callback( "Saving audio files is not supported on iOS" );
return;
}
Debug.Log( "Saving to Pictures: " + Path.GetFileName( path ) );
NGMediaSaveCallbackiOS.Initialize( callback );
if( mediaType == MediaType.Image )
_NativeGallery_ImageWriteToAlbum( path, album );
else if( mediaType == MediaType.Video )
_NativeGallery_VideoWriteToAlbum( path, album );
#else
if( callback != null )
callback( 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( true );
if( result == Permission.Granted && !IsMediaPickerBusy() )
{
#if !UNITY_EDITOR && UNITY_ANDROID
string savePath;
if( mediaType == MediaType.Image )
savePath = SelectedImagePath;
else if( mediaType == MediaType.Video )
savePath = SelectedVideoPath;
else
savePath = SelectedAudioPath;
AJC.CallStatic( "PickMedia", Context, new NGMediaReceiveCallbackAndroid( callback, null ), (int) mediaType, false, savePath, mime, title );
#elif !UNITY_EDITOR && UNITY_IOS
NGMediaReceiveCallbackiOS.Initialize( callback );
if( mediaType == MediaType.Image )
_NativeGallery_PickImage( SelectedImagePath );
else if( mediaType == MediaType.Video )
_NativeGallery_PickVideo( SelectedVideoPath );
else if( callback != null ) // Selecting audio files is not supported on iOS
callback( null );
#else
if( callback != null )
callback( null );
#endif
}
return result;
}
private static Permission GetMultipleMediaFromGallery( MediaPickMultipleCallback callback, MediaType mediaType, string mime, string title )
{
Permission result = RequestPermission( true );
if( result == Permission.Granted && !IsMediaPickerBusy() )
{
if( CanSelectMultipleFilesFromGallery() )
{
#if !UNITY_EDITOR && UNITY_ANDROID
string savePath;
if( mediaType == MediaType.Image )
savePath = SelectedImagePath;
else if( mediaType == MediaType.Video )
savePath = SelectedVideoPath;
else
savePath = SelectedAudioPath;
AJC.CallStatic( "PickMedia", Context, new NGMediaReceiveCallbackAndroid( null, callback ), (int) mediaType, true, savePath, mime, title );
#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, texture.format, 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 );
}
}
#endregion
#region Utility Functions
public static Texture2D LoadImageAtPath( string imagePath, int maxSize = -1, bool markTextureNonReadable = true,
bool generateMipmaps = true, bool linearColorSpace = false )
{
if( string.IsNullOrEmpty( imagePath ) )
throw new ArgumentException( "Parameter 'imagePath' is null or empty!" );
if( !File.Exists( imagePath ) )
throw new FileNotFoundException( "File not found at " + imagePath );
if( maxSize <= 0 )
maxSize = SystemInfo.maxTextureSize;
#if !UNITY_EDITOR && UNITY_ANDROID
string loadPath = AJC.CallStatic<string>( "LoadImageAtPath", Context, imagePath, TemporaryImagePath, maxSize );
#elif !UNITY_EDITOR && UNITY_IOS
string loadPath = _NativeGallery_LoadImageAtPath( imagePath, TemporaryImagePath, maxSize );
#else
string loadPath = imagePath;
#endif
String extension = Path.GetExtension( imagePath ).ToLowerInvariant();
TextureFormat format = ( extension == ".jpg" || extension == ".jpeg" ) ? TextureFormat.RGB24 : TextureFormat.RGBA32;
Texture2D result = new Texture2D( 2, 2, format, generateMipmaps, linearColorSpace );
try
{
if( !result.LoadImage( File.ReadAllBytes( loadPath ), markTextureNonReadable ) )
{
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;
}
public static Texture2D GetVideoThumbnail( string videoPath, int maxSize = -1, double captureTimeInSeconds = -1.0 )
{
if( maxSize <= 0 )
maxSize = SystemInfo.maxTextureSize;
#if !UNITY_EDITOR && UNITY_ANDROID
string thumbnailPath = AJC.CallStatic<string>( "GetVideoThumbnail", Context, videoPath, TemporaryImagePath + ".png", false, maxSize, captureTimeInSeconds );
#elif !UNITY_EDITOR && UNITY_IOS
string thumbnailPath = _NativeGallery_GetVideoThumbnail( videoPath, TemporaryImagePath + ".png", maxSize, captureTimeInSeconds );
#else
string thumbnailPath = null;
#endif
if( !string.IsNullOrEmpty( thumbnailPath ) )
return LoadImageAtPath( thumbnailPath, maxSize );
else
return null;
}
public static ImageProperties GetImageProperties( string imagePath )
{
if( !File.Exists( imagePath ) )
throw new FileNotFoundException( "File not found at " + imagePath );
#if !UNITY_EDITOR && UNITY_ANDROID
string value = AJC.CallStatic<string>( "GetImageProperties", Context, imagePath );
#elif !UNITY_EDITOR && UNITY_IOS
string value = _NativeGallery_GetImageProperties( imagePath );
#else
string value = null;
#endif
int width = 0, height = 0;
string mimeType = null;
ImageOrientation orientation = ImageOrientation.Unknown;
if( !string.IsNullOrEmpty( value ) )
{
string[] properties = value.Split( '>' );
if( properties != null && properties.Length >= 4 )
{
if( !int.TryParse( properties[0].Trim(), out width ) )
width = 0;
if( !int.TryParse( properties[1].Trim(), out height ) )
height = 0;
mimeType = properties[2].Trim();
if( mimeType.Length == 0 )
{
String extension = Path.GetExtension( imagePath ).ToLowerInvariant();
if( extension == ".png" )
mimeType = "image/png";
else if( extension == ".jpg" || extension == ".jpeg" )
mimeType = "image/jpeg";
else if( extension == ".gif" )
mimeType = "image/gif";
else if( extension == ".bmp" )
mimeType = "image/bmp";
else
mimeType = null;
}
int orientationInt;
if( int.TryParse( properties[3].Trim(), out orientationInt ) )
orientation = (ImageOrientation) orientationInt;
}
}
return new ImageProperties( width, height, mimeType, orientation );
}
public static VideoProperties GetVideoProperties( string videoPath )
{
if( !File.Exists( videoPath ) )
throw new FileNotFoundException( "File not found at " + videoPath );
#if !UNITY_EDITOR && UNITY_ANDROID
string value = AJC.CallStatic<string>( "GetVideoProperties", Context, videoPath );
#elif !UNITY_EDITOR && UNITY_IOS
string value = _NativeGallery_GetVideoProperties( videoPath );
#else
string value = null;
#endif
int width = 0, height = 0;
long duration = 0L;
float rotation = 0f;
if( !string.IsNullOrEmpty( value ) )
{
string[] properties = value.Split( '>' );
if( properties != null && properties.Length >= 4 )
{
if( !int.TryParse( properties[0].Trim(), out width ) )
width = 0;
if( !int.TryParse( properties[1].Trim(), out height ) )
height = 0;
if( !long.TryParse( properties[2].Trim(), out duration ) )
duration = 0L;
if( !float.TryParse( properties[3].Trim(), out rotation ) )
rotation = 0f;
}
}
if( rotation == -90f )
rotation = 270f;
return new VideoProperties( width, height, duration, rotation );
}
#endregion
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 89147d0c91efd4310bf3b6ab773df35b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,113 @@
= Native Gallery for Android & iOS =
Online documentation & example code available at: https://github.com/yasirkula/UnityNativeGallery
E-mail: yasirkula@gmail.com
1. ABOUT
This plugin helps you interact with Gallery/Photos on Android & iOS.
2. HOW TO
for Android: set "Write Permission" to "External (SDCard)" in Player Settings
for iOS: there are two ways to set up the plugin on iOS:
a. Automated Setup for iOS
- (optional) change the value of PHOTO_LIBRARY_USAGE_DESCRIPTION in Plugins/NativeGallery/Editor/NGPostProcessBuild.cs
- (Unity 2017.4 or earlier) if your minimum Deployment Target (iOS Version) is at least 8.0, set the value of MINIMUM_TARGET_8_OR_ABOVE to true in NGPostProcessBuild.cs
b. Manual Setup for iOS
- set the value of ENABLED to false in NGPostProcessBuild.cs
- build your project
- enter a Photo Library Usage Description to Info.plist in Xcode
- also enter a "Photo Library Additions Usage Description" to Info.plist in Xcode, if exists
- insert "-weak_framework Photos -framework AssetsLibrary -framework MobileCoreServices -framework ImageIO" to the "Other Linker Flags" of Unity-iPhone Target (if your Deployment Target is at least 8.0, it is sufficient to insert "-framework Photos -framework MobileCoreServices -framework ImageIO")
- lastly, remove Photos.framework from Link Binary With Libraries of Unity-iPhone Target in Build Phases, if exists
3. FAQ
- How can I fetch the path of the saved image or the original path of the picked image?
You can't. On iOS, these files are stored in an internal directory that we have no access to (I don't think there is even a way to fetch that internal path). On Android, with Storage Access Framework, the absolute path is hidden behind the SAF-layer. There are some tricks here and there to convert SAF-path to absolute path but they don't work in all cases (most of the snippets that can be found on Stackoverflow can't return the absolute path if the file is stored on external SD card).
- Can't access the Gallery, it says "java.lang.ClassNotFoundException: com.yasirkula.unity.NativeGallery" in Logcat
If your project uses ProGuard, try adding the following line to ProGuard filters: -keep class com.yasirkula.unity.* { *; }
- Nothing happens when I try to access the Gallery on Android
Make sure that you've set the "Write Permission" to "External (SDCard)" in Player Settings.
- Saving image/video doesn't work properly
Make sure that the "filename" parameter of the Save function includes the file's extension, as well
4. SCRIPTING API
Please see the online documentation for a more in-depth documentation of the Scripting API: https://github.com/yasirkula/UnityNativeGallery
enum NativeGallery.Permission { Denied = 0, Granted = 1, ShouldAsk = 2 };
enum NativeGallery.ImageOrientation { Unknown = -1, Normal = 0, Rotate90 = 1, Rotate180 = 2, Rotate270 = 3, FlipHorizontal = 4, Transpose = 5, FlipVertical = 6, Transverse = 7 }; // EXIF orientation: http://sylvana.net/jpegcrop/exif_orientation.html (indices are reordered)
delegate void MediaSaveCallback( string error );
delegate void NativeGallery.MediaPickCallback( string path );
delegate void MediaPickMultipleCallback( string[] paths );
//// Saving Media To Gallery/Photos ////
// On Android, your images are saved at DCIM/album/filename. On iOS, the image will be saved in the corresponding album.
// NOTE: Make sure that the filename parameter includes the file's extension, as well
// IMPORTANT: NativeGallery will never overwrite existing media on the Gallery. If there is a name conflict, NativeGallery will ensure a unique filename. So don't put '{0}' in filename anymore (for new users, putting {0} in filename was recommended in order to ensure unique filenames in earlier versions, this is no longer necessary).
// MediaSaveCallback takes a string parameter which stores an error string if something goes wrong while saving the image/video, or null if it is saved successfully
NativeGallery.Permission NativeGallery.SaveImageToGallery( byte[] mediaBytes, string album, string filename, MediaSaveCallback callback = null );
NativeGallery.Permission NativeGallery.SaveImageToGallery( string existingMediaPath, string album, string filename, MediaSaveCallback callback = null );
NativeGallery.Permission NativeGallery.SaveImageToGallery( Texture2D image, string album, string filename, MediaSaveCallback callback = null );
NativeGallery.Permission NativeGallery.SaveVideoToGallery( byte[] mediaBytes, string album, string filename, MediaSaveCallback callback = null );
NativeGallery.Permission NativeGallery.SaveVideoToGallery( string existingMediaPath, string album, string filename, MediaSaveCallback callback = null );
//// Retrieving Media From Gallery/Photos ////
// This operation is asynchronous! After user selects an image/video or cancels the operation, the callback is called (on main thread)
// MediaPickCallback takes a string parameter which stores the path of the selected image/video, or null if nothing is selected
// MediaPickMultipleCallback takes a string[] parameter which stores the path(s) of the selected image(s)/video(s), or null if nothing is selected
// title: determines the title of the image picker dialog on Android. Has no effect on iOS
// mime: filters the available images/videos on Android. For example, to request a JPEG image from the user, mime can be set as "image/jpeg". Setting multiple mime types is not possible (in that case, you should leave mime as is). Has no effect on iOS
NativeGallery.Permission NativeGallery.GetImageFromGallery( MediaPickCallback callback, string title = "", string mime = "image/*" );
NativeGallery.Permission NativeGallery.GetVideoFromGallery( MediaPickCallback callback, string title = "", string mime = "video/*" );
NativeGallery.Permission NativeGallery.GetImagesFromGallery( MediaPickMultipleCallback callback, string title = "", string mime = "image/*" );
NativeGallery.Permission NativeGallery.GetVideosFromGallery( MediaPickMultipleCallback callback, string title = "", string mime = "video/*" );
// Returns true if selecting multiple images/videos from Gallery/Photos is possible on this device (only available on Android 18 and later; iOS not supported)
bool NativeGallery.CanSelectMultipleFilesFromGallery();
// Returns true if the user is currently picking media from Gallery/Photos. In that case, another GetImageFromGallery or GetVideoFromGallery request will simply be ignored
bool NativeGallery.IsMediaPickerBusy();
//// Runtime Permissions ////
// Interacting with Gallery/Photos is only possible when permission state is Permission.Granted. Most of the functions request permission internally (and return the result) but you can also check/request the permissions manually
NativeGallery.Permission NativeGallery.CheckPermission();
NativeGallery.Permission NativeGallery.RequestPermission();
// If permission state is Permission.Denied, user must grant the necessary permission (Storage on Android and Photos on iOS) manually from the Settings. These functions help you open the Settings directly from within the app
void NativeGallery.OpenSettings();
bool NativeGallery.CanOpenSettings();
//// Utility Functions ////
// Creates a Texture2D from the specified image file in correct orientation and returns it. Returns null, if something goes wrong
// maxSize: determines the maximum size of the returned Texture2D in pixels. Larger textures will be down-scaled. If untouched, its value will be set to SystemInfo.maxTextureSize. It is recommended to set a proper maxSize for better performance
// markTextureNonReadable: marks the generated texture as non-readable for better memory usage. If you plan to modify the texture later (e.g. GetPixels/SetPixels), set its value to false
// generateMipmaps: determines whether texture should have mipmaps or not
// linearColorSpace: determines whether texture should be in linear color space or sRGB color space
Texture2D NativeGallery.LoadImageAtPath( string imagePath, int maxSize = -1, bool markTextureNonReadable = true, bool generateMipmaps = true, bool linearColorSpace = false );
// Creates a Texture2D thumbnail from a video file and returns it. Returns null, if something goes wrong
// maxSize: determines the maximum size of the returned Texture2D in pixels. Larger thumbnails will be down-scaled. If untouched, its value will be set to SystemInfo.maxTextureSize. It is recommended to set a proper maxSize for better performance
// captureTimeInSeconds: determines the frame of the video that the thumbnail is captured from. If untouched, OS will decide this value
Texture2D NativeGallery.GetVideoThumbnail( string videoPath, int maxSize = -1, double captureTimeInSeconds = -1.0 );
// Returns an ImageProperties instance that holds the width, height and mime type information of an image file without creating a Texture2D object. Mime type will be null, if it can't be determined
NativeGallery.ImageProperties NativeGallery.GetImageProperties( string imagePath );
// Returns a VideoProperties instance that holds the width, height, duration (in milliseconds) and rotation information of a video file. To play a video in correct orientation, you should rotate it by rotation degrees clockwise. For a 90-degree or 270-degree rotated video, values of width and height should be swapped to get the display size of the video
NativeGallery.VideoProperties NativeGallery.GetVideoProperties( string videoPath );

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 1852f26c8592c453f8f7423bf2a283d7
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c8af1e45d8c954cd0a87623adcbc246d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,72 @@
#if !UNITY_EDITOR && UNITY_IOS
using UnityEngine;
namespace NativeGalleryNamespace
{
public class NGMediaReceiveCallbackiOS : MonoBehaviour
{
private static NGMediaReceiveCallbackiOS instance;
private NativeGallery.MediaPickCallback callback;
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 )
{
if( IsBusy )
return;
if( instance == null )
{
instance = new GameObject( "NGMediaReceiveCallbackiOS" ).AddComponent<NGMediaReceiveCallbackiOS>();
DontDestroyOnLoad( instance.gameObject );
}
instance.callback = callback;
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;
if( callback != null )
{
callback( null );
callback = null;
}
}
}
}
}
public void OnMediaReceived( string path )
{
IsBusy = false;
if( string.IsNullOrEmpty( path ) )
path = null;
if( callback != null )
{
callback( path );
callback = null;
}
}
}
}
#endif

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 71fb861c149c2d1428544c601e52a33c
timeCreated: 1519060539
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,46 @@
#if !UNITY_EDITOR && UNITY_IOS
using UnityEngine;
namespace NativeGalleryNamespace
{
public class NGMediaSaveCallbackiOS : MonoBehaviour
{
private static NGMediaSaveCallbackiOS instance;
private NativeGallery.MediaSaveCallback callback;
public static void Initialize( NativeGallery.MediaSaveCallback callback )
{
if( instance == null )
{
instance = new GameObject( "NGMediaSaveCallbackiOS" ).AddComponent<NGMediaSaveCallbackiOS>();
DontDestroyOnLoad( instance.gameObject );
}
else if( instance.callback != null )
instance.callback( null );
instance.callback = callback;
}
public void OnMediaSaveCompleted( string message )
{
if( callback != null )
{
callback( null );
callback = null;
}
}
public void OnMediaSaveFailed( string error )
{
if( string.IsNullOrEmpty( error ) )
error = "Unknown error";
if( callback != null )
{
callback( error );
callback = null;
}
}
}
}
#endif

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 9cbb865d0913a0d47bb6d2eb3ad04c4f
timeCreated: 1519060539
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,781 @@
#import <Foundation/Foundation.h>
#import <Photos/Photos.h>
#import <MobileCoreServices/UTCoreTypes.h>
#import <ImageIO/ImageIO.h>
#if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000
#import <AssetsLibrary/AssetsLibrary.h>
#endif
#ifdef UNITY_4_0 || UNITY_5_0
#import "iPhone_View.h"
#else
extern UIViewController* UnityGetGLViewController();
#endif
@interface UNativeGallery:NSObject
+ (int)checkPermission;
+ (int)requestPermission;
+ (int)canOpenSettings;
+ (void)openSettings;
+ (void)saveMedia:(NSString *)path albumName:(NSString *)album isImg:(BOOL)isImg;
+ (void)pickMedia:(BOOL)imageMode savePath:(NSString *)mediaSavePath;
+ (int)isMediaPickerBusy;
+ (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 NSString *resultPath;
static UIPopoverController *popup;
static UIImagePickerController *imagePicker;
static int imagePickerState = 0; // 0 -> none, 1 -> showing (always in this state on iPad), 2 -> finished
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+ (int)checkPermission {
#if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000
if ([[[UIDevice currentDevice] systemVersion] compare:@"8.0" options:NSNumericSearch] != NSOrderedAscending)
{
#endif
// version >= iOS 8: check permission using Photos framework
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 {
#if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000
if ([[[UIDevice currentDevice] systemVersion] compare:@"8.0" options:NSNumericSearch] != NSOrderedAscending)
{
#endif
// version >= iOS 8: request permission using Photos framework
return [self requestPermissionNew];
#if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000
}
else
{
// version < iOS 8: request permission using AssetsLibrary framework (Photos framework not available)
return [self requestPermissionOld];
}
#endif
}
// Credit: https://stackoverflow.com/a/25453667/2373034
+ (int)canOpenSettings {
if (&UIApplicationOpenSettingsURLString != NULL)
return 1;
else
return 0;
}
// Credit: https://stackoverflow.com/a/25453667/2373034
+ (void)openSettings {
if (&UIApplicationOpenSettingsURLString != NULL)
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]];
}
+ (void)saveMedia:(NSString *)path albumName:(NSString *)album isImg:(BOOL)isImg {
#if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000
if ([[[UIDevice currentDevice] systemVersion] compare:@"8.0" options:NSNumericSearch] != NSOrderedAscending)
{
#endif
// version >= iOS 8: save to specified album using Photos framework
[self saveMediaNew:path albumName:album isImage:isImg];
#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
}
// Credit: https://stackoverflow.com/a/26933380/2373034
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+ (int)requestPermissionOld {
#if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000
ALAuthorizationStatus status = [ALAssetsLibrary authorizationStatus];
if (status == ALAuthorizationStatusAuthorized) {
return 1;
}
else if (status == ALAuthorizationStatusNotDetermined) {
__block BOOL authorized = NO;
ALAssetsLibrary *lib = [[ALAssetsLibrary alloc] init];
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[lib 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);
if (authorized)
return 1;
else
return 0;
}
else {
return 0;
}
#endif
return 0;
}
#pragma clang diagnostic pop
// Credit: https://stackoverflow.com/a/32989022/2373034
+ (int)requestPermissionNew {
PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus];
if (status == PHAuthorizationStatusAuthorized) {
return 1;
}
else if (status == PHAuthorizationStatusNotDetermined) {
__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);
if (authorized)
return 1;
else
return 0;
}
else {
return 0;
}
}
// 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 {
#if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000
ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
if (!isImage && ![library videoAtPathIsCompatibleWithSavedPhotosAlbum:[NSURL fileURLWithPath:path]])
{
[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
UnitySendMessage("NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "Video format is not compatible with Photos");
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", [self getCString:[error localizedDescription]]);
}];
}
else {
NSLog(@"Error creating asset: %@", error);
UnitySendMessage("NGMediaSaveCallbackiOS", "OnMediaSaveFailed", [self getCString:[error localizedDescription]]);
}
};
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", [self getCString:[error localizedDescription]]);
}];
}
} failureBlock:^(NSError* error) {
NSLog(@"Error listing albums: %@", error);
[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
UnitySendMessage("NGMediaSaveCallbackiOS", "OnMediaSaveFailed", [self getCString:[error localizedDescription]]);
}];
#endif
}
#pragma clang diagnostic pop
// Credit: https://stackoverflow.com/a/39909129/2373034
+ (void)saveMediaNew:(NSString *)path albumName:(NSString *)album isImage:(BOOL)isImage {
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) {
[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
if (success)
UnitySendMessage("NGMediaSaveCallbackiOS", "OnMediaSaveCompleted", "");
else {
NSLog(@"Error creating asset: %@", error);
UnitySendMessage("NGMediaSaveCallbackiOS", "OnMediaSaveFailed", [self getCString:[error localizedDescription]]);
}
}];
};
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 {
[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
UnitySendMessage("NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "Album placeholder not found" );
}
}
else {
NSLog(@"Error creating album: %@", error);
[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
UnitySendMessage("NGMediaSaveCallbackiOS", "OnMediaSaveFailed", [self getCString:[error localizedDescription]]);
}
}];
}
}
// Credit: https://stackoverflow.com/a/10531752/2373034
+ (void)pickMedia:(BOOL)imageMode savePath:(NSString *)mediaSavePath {
imagePicker = [[UIImagePickerController alloc] init];
imagePicker.delegate = self;
imagePicker.allowsEditing = NO;
imagePicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
if (imageMode)
imagePicker.mediaTypes = [NSArray arrayWithObject:(NSString *)kUTTypeImage];
else
{
imagePicker.mediaTypes = [NSArray arrayWithObjects:(NSString *)kUTTypeMovie, (NSString *)kUTTypeVideo, nil];
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000
// Don't compress the picked video if possible
if ([[[UIDevice currentDevice] systemVersion] compare:@"11.0" options:NSNumericSearch] != NSOrderedAscending)
imagePicker.videoExportPreset = AVAssetExportPresetPassthrough;
#endif
}
pickedMediaSavePath = mediaSavePath;
imagePickerState = 1;
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 = 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;
}
}
else
return 0;
}
// Credit: https://stackoverflow.com/a/4170099/2373034
+ (NSArray *)getImageMetadata:(NSString *)path {
int width = 0;
int height = 0;
int orientation = -1;
CGImageSourceRef imageSource = CGImageSourceCreateWithURL((__bridge CFURLRef)[NSURL fileURLWithPath:path], nil);
if (imageSource != nil) {
NSDictionary *options = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:NO] forKey:(__bridge NSString *)kCGImageSourceShouldCache];
CFDictionaryRef imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, (__bridge CFDictionaryRef)options);
CFRelease(imageSource);
CGFloat widthF = 0.0f, heightF = 0.0f;
if (imageProperties != nil) {
if (CFDictionaryContainsKey(imageProperties, kCGImagePropertyPixelWidth))
CFNumberGetValue((CFNumberRef)CFDictionaryGetValue(imageProperties, kCGImagePropertyPixelWidth), kCFNumberCGFloatType, &widthF);
if (CFDictionaryContainsKey(imageProperties, kCGImagePropertyPixelHeight))
CFNumberGetValue((CFNumberRef)CFDictionaryGetValue(imageProperties, kCGImagePropertyPixelHeight), kCFNumberCGFloatType, &heightF);
if (CFDictionaryContainsKey(imageProperties, kCGImagePropertyOrientation)) {
CFNumberGetValue((CFNumberRef)CFDictionaryGetValue(imageProperties, kCGImagePropertyOrientation), kCFNumberIntType, &orientation);
if (orientation > 4) { // landscape image
CGFloat temp = widthF;
widthF = heightF;
heightF = temp;
}
}
CFRelease(imageProperties);
}
width = (int)roundf(widthF);
height = (int)roundf(heightF);
}
return [[NSArray alloc] initWithObjects:[NSNumber numberWithInt:width], [NSNumber numberWithInt:height], [NSNumber numberWithInt:orientation], nil];
}
+ (char *)getImageProperties:(NSString *)path {
NSArray *metadata = [self getImageMetadata:path];
int orientationUnity;
int orientation = [metadata[2] intValue];
// To understand the magic numbers, see ImageOrientation enum in NativeGallery.cs
// and http://sylvana.net/jpegcrop/exif_orientation.html
if (orientation == 1)
orientationUnity = 0;
else if (orientation == 2)
orientationUnity = 4;
else if (orientation == 3)
orientationUnity = 2;
else if (orientation == 4)
orientationUnity = 6;
else if (orientation == 5)
orientationUnity = 5;
else if (orientation == 6)
orientationUnity = 1;
else if (orientation == 7)
orientationUnity = 7;
else if (orientation == 8)
orientationUnity = 3;
else
orientationUnity = -1;
return [self getCString:[NSString stringWithFormat:@"%d>%d> >%d", [metadata[0] intValue], [metadata[1] intValue], orientationUnity]];
}
+ (char *)getVideoProperties:(NSString *)path {
CGSize size = CGSizeZero;
float rotation = 0;
long long duration = 0;
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:[NSURL fileURLWithPath:path] options:nil];
if (asset != nil) {
duration = (long long) round(CMTimeGetSeconds([asset duration]) * 1000);
CGAffineTransform transform = [asset preferredTransform];
NSArray<AVAssetTrack *>* videoTracks = [asset tracksWithMediaType:AVMediaTypeVideo];
if (videoTracks != nil && [videoTracks count] > 0) {
size = [[videoTracks objectAtIndex:0] naturalSize];
transform = [[videoTracks objectAtIndex:0] preferredTransform];
}
rotation = atan2(transform.b, transform.a) * (180.0 / M_PI);
}
return [self getCString:[NSString stringWithFormat:@"%d>%d>%lld>%f", (int)roundf(size.width), (int)roundf(size.height), duration, rotation]];
}
+ (char *)getVideoThumbnail:(NSString *)path savePath:(NSString *)savePath maximumSize:(int)maximumSize captureTime:(double)captureTime {
AVAssetImageGenerator *thumbnailGenerator = [[AVAssetImageGenerator alloc] initWithAsset:[[AVURLAsset alloc] initWithURL:[NSURL fileURLWithPath:path] options:nil]];
thumbnailGenerator.appliesPreferredTrackTransform = YES;
thumbnailGenerator.maximumSize = CGSizeMake((CGFloat) maximumSize, (CGFloat) maximumSize);
thumbnailGenerator.requestedTimeToleranceBefore = kCMTimeZero;
thumbnailGenerator.requestedTimeToleranceAfter = kCMTimeZero;
if (captureTime < 0.0)
captureTime = 0.0;
else {
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:[NSURL fileURLWithPath:path] options:nil];
if (asset != nil) {
double videoDuration = CMTimeGetSeconds([asset duration]);
if (videoDuration > 0.0 && captureTime >= videoDuration - 0.1) {
if (captureTime > videoDuration)
captureTime = videoDuration;
thumbnailGenerator.requestedTimeToleranceBefore = CMTimeMakeWithSeconds(1.0, 600);
}
}
}
NSError *error = nil;
CGImageRef image = [thumbnailGenerator copyCGImageAtTime:CMTimeMakeWithSeconds(captureTime, 600) actualTime:nil error:&error];
if (image == nil) {
if (error != nil)
NSLog(@"Error generating video thumbnail: %@", error);
else
NSLog(@"Error generating video thumbnail...");
return "";
}
UIImage *thumbnail = [[UIImage alloc] initWithCGImage:image];
CGImageRelease(image);
if (![UIImagePNGRepresentation(thumbnail) writeToFile:savePath atomically:YES]) {
NSLog(@"Error saving thumbnail image");
return "";
}
return [self getCString:savePath];
}
+ (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);
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];
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+ (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {
resultPath = nil;
if ([info[UIImagePickerControllerMediaType] isEqualToString:(NSString *)kUTTypeImage]) { // image picked
// On iOS 8.0 or later, try to obtain the raw data of the image (which allows picking gifs properly or preserving metadata)
if ([[[UIDevice currentDevice] systemVersion] compare:@"8.0" options:NSNumericSearch] != NSOrderedAscending) {
PHAsset *asset = nil;
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000
if ([[[UIDevice currentDevice] systemVersion] compare:@"11.0" options:NSNumericSearch] != NSOrderedAscending) {
// 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) {
if (asset == nil) {
NSURL *mediaUrl = info[UIImagePickerControllerReferenceURL] ?: info[UIImagePickerControllerMediaURL];
if (mediaUrl != nil)
asset = [[PHAsset fetchAssetsWithALAssetURLs:[NSArray arrayWithObject:mediaUrl] options:nil] firstObject];
}
if (asset != nil) {
PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init];
options.synchronous = YES;
options.version = PHImageRequestOptionsVersionCurrent;
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
if ([[[UIDevice currentDevice] systemVersion] compare:@"13.0" options:NSNumericSearch] != NSOrderedAscending) {
[[PHImageManager defaultManager] requestImageDataAndOrientationForAsset:asset options:options resultHandler:^(NSData *imageData, NSString *dataUTI, CGImagePropertyOrientation orientation, NSDictionary *imageInfo) {
if (imageData != nil)
[self trySaveSourceImage:imageData withInfo:imageInfo];
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)
[self trySaveSourceImage:imageData withInfo:imageInfo];
else
NSLog(@"Couldn't fetch raw image data");
}];
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
}
#endif
}
}
}
if (resultPath == nil) {
// Temporarily save image as PNG
UIImage *image = info[UIImagePickerControllerOriginalImage];
if (image != nil) {
resultPath = [pickedMediaSavePath stringByAppendingPathExtension:@"png"];
if (![UIImagePNGRepresentation(image) writeToFile:resultPath atomically:YES]) {
NSLog(@"Error creating PNG image");
resultPath = nil;
}
}
else
NSLog(@"Error fetching original image from picker");
}
}
else { // video picked
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 ([[[UIDevice currentDevice] systemVersion] compare:@"13.0" options:NSNumericSearch] != NSOrderedAscending) {
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
+ (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker
{
popup = nil;
imagePicker = nil;
UnitySendMessage("NGMediaReceiveCallbackiOS", "OnMediaReceived", "");
[picker dismissViewControllerAnimated:YES completion:nil];
}
+ (void)popoverControllerDidDismissPopover:(UIPopoverController *)popoverController {
popup = nil;
imagePicker = nil;
UnitySendMessage("NGMediaReceiveCallbackiOS", "OnMediaReceived", "");
}
+ (void)trySaveSourceImage:(NSData *)imageData withInfo:(NSDictionary *)info {
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];
}
if (filePath == nil || [filePath length] == 0)
resultPath = pickedMediaSavePath;
else
resultPath = [pickedMediaSavePath 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;
}
}
// 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() {
return [UNativeGallery checkPermission];
}
extern "C" int _NativeGallery_RequestPermission() {
return [UNativeGallery requestPermission];
}
extern "C" int _NativeGallery_CanOpenSettings() {
return [UNativeGallery canOpenSettings];
}
extern "C" void _NativeGallery_OpenSettings() {
[UNativeGallery openSettings];
}
extern "C" void _NativeGallery_ImageWriteToAlbum(const char* path, const char* album) {
[UNativeGallery saveMedia:[NSString stringWithUTF8String:path] albumName:[NSString stringWithUTF8String:album] isImg:YES];
}
extern "C" void _NativeGallery_VideoWriteToAlbum(const char* path, const char* album) {
[UNativeGallery saveMedia:[NSString stringWithUTF8String:path] albumName:[NSString stringWithUTF8String:album] isImg:NO];
}
extern "C" void _NativeGallery_PickImage(const char* imageSavePath) {
[UNativeGallery pickMedia:YES savePath:[NSString stringWithUTF8String:imageSavePath]];
}
extern "C" void _NativeGallery_PickVideo(const char* videoSavePath) {
[UNativeGallery pickMedia:NO savePath:[NSString stringWithUTF8String:videoSavePath]];
}
extern "C" int _NativeGallery_IsMediaPickerBusy() {
return [UNativeGallery isMediaPickerBusy];
}
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];
}

View File

@@ -0,0 +1,33 @@
fileFormatVersion: 2
guid: 953e0b740eb03144883db35f72cad8a6
timeCreated: 1498722774
licenseType: Pro
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: