This commit is contained in:
2020-07-04 14:41:25 +08:00
parent 70c346d2c1
commit a8f02e4da5
3748 changed files with 587372 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 0a607dcda26e7614f86300c6ca717295
folderAsset: yes
timeCreated: 1498722617
licenseType: Store
DefaultImporter:
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: Store
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: Store
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: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

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

View File

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

View File

@@ -0,0 +1,63 @@
#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 );
string targetGUID = pbxProject.TargetGuidByName( PBXProject.GetUnityTargetName() );
if( MINIMUM_TARGET_8_OR_ABOVE )
{
pbxProject.AddBuildProperty( targetGUID, "OTHER_LDFLAGS", "-framework Photos" );
pbxProject.AddBuildProperty( targetGUID, "OTHER_LDFLAGS", "-framework MobileCoreServices" );
pbxProject.AddBuildProperty( targetGUID, "OTHER_LDFLAGS", "-framework ImageIO" );
}
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" );
}
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: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,665 @@
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 };
// 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, int maxSize );
[System.Runtime.InteropServices.DllImport( "__Internal" )]
private static extern void _NativeGallery_PickVideo();
[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_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;
}
}
#endif
#if !UNITY_EDITOR && UNITY_IOS
private static string m_iOSSelectedImagePath = null;
private static string IOSSelectedImagePath
{
get
{
if( m_iOSSelectedImagePath == null )
{
m_iOSSelectedImagePath = Path.Combine( Application.temporaryCachePath, "tmp.png" );
Directory.CreateDirectory( Application.temporaryCachePath );
}
return m_iOSSelectedImagePath;
}
}
#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 filenameFormatted, MediaSaveCallback callback = null )
{
return SaveToGallery( mediaBytes, album, filenameFormatted, true, callback );
}
public static Permission SaveImageToGallery( string existingMediaPath, string album, string filenameFormatted, MediaSaveCallback callback = null )
{
return SaveToGallery( existingMediaPath, album, filenameFormatted, true, callback );
}
public static Permission SaveImageToGallery( Texture2D image, string album, string filenameFormatted, MediaSaveCallback callback = null )
{
if( image == null )
throw new ArgumentException( "Parameter 'image' is null!" );
if( filenameFormatted.EndsWith( ".jpeg" ) || filenameFormatted.EndsWith( ".jpg" ) )
return SaveToGallery( GetTextureBytes( image, true ), album, filenameFormatted, true, callback );
else if( filenameFormatted.EndsWith( ".png" ) )
return SaveToGallery( GetTextureBytes( image, false ), album, filenameFormatted, true, callback );
else
return SaveToGallery( GetTextureBytes( image, false ), album, filenameFormatted + ".png", true, callback );
}
public static Permission SaveVideoToGallery( byte[] mediaBytes, string album, string filenameFormatted, MediaSaveCallback callback = null )
{
return SaveToGallery( mediaBytes, album, filenameFormatted, false, callback );
}
public static Permission SaveVideoToGallery( string existingMediaPath, string album, string filenameFormatted, MediaSaveCallback callback = null )
{
return SaveToGallery( existingMediaPath, album, filenameFormatted, false, 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/*", int maxSize = -1 )
{
return GetMediaFromGallery( callback, true, mime, title, maxSize );
}
public static Permission GetVideoFromGallery( MediaPickCallback callback, string title = "", string mime = "video/*" )
{
return GetMediaFromGallery( callback, false, mime, title, -1 );
}
public static Permission GetImagesFromGallery( MediaPickMultipleCallback callback, string title = "", string mime = "image/*", int maxSize = -1 )
{
return GetMultipleMediaFromGallery( callback, true, mime, title, maxSize );
}
public static Permission GetVideosFromGallery( MediaPickMultipleCallback callback, string title = "", string mime = "video/*" )
{
return GetMultipleMediaFromGallery( callback, false, mime, title, -1 );
}
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 filenameFormatted, bool isImage, 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( filenameFormatted == null || filenameFormatted.Length == 0 )
throw new ArgumentException( "Parameter 'filenameFormatted' is null or empty!" );
string path = GetSavePath( album, filenameFormatted );
File.WriteAllBytes( path, mediaBytes );
SaveToGalleryInternal( path, album, isImage, callback );
}
return result;
}
private static Permission SaveToGallery( string existingMediaPath, string album, string filenameFormatted, bool isImage, 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( filenameFormatted == null || filenameFormatted.Length == 0 )
throw new ArgumentException( "Parameter 'filenameFormatted' is null or empty!" );
string path = GetSavePath( album, filenameFormatted );
File.Copy( existingMediaPath, path, true );
SaveToGalleryInternal( path, album, isImage, callback );
}
return result;
}
private static void SaveToGalleryInternal( string path, string album, bool isImage, MediaSaveCallback callback )
{
#if !UNITY_EDITOR && UNITY_ANDROID
AJC.CallStatic( "MediaScanFile", Context, path );
if( callback != null )
callback( null );
Debug.Log( "Saving to gallery: " + path );
#elif !UNITY_EDITOR && UNITY_IOS
NGMediaSaveCallbackiOS.Initialize( callback );
if( isImage )
_NativeGallery_ImageWriteToAlbum( path, album );
else
_NativeGallery_VideoWriteToAlbum( path, album );
Debug.Log( "Saving to Pictures: " + Path.GetFileName( path ) );
#else
if( callback != null )
callback( null );
#endif
}
private static string GetSavePath( string album, string filenameFormatted )
{
string saveDir;
#if !UNITY_EDITOR && UNITY_ANDROID
saveDir = AJC.CallStatic<string>( "GetMediaPath", album );
#else
saveDir = Application.persistentDataPath;
#endif
if( filenameFormatted.Contains( "{0}" ) )
{
int fileIndex = 0;
string path;
do
{
path = Path.Combine( saveDir, string.Format( filenameFormatted, ++fileIndex ) );
} while( File.Exists( path ) );
return path;
}
saveDir = Path.Combine( saveDir, filenameFormatted );
#if !UNITY_EDITOR && UNITY_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
if( File.Exists( saveDir ) )
{
return GetSavePath( album,
Path.GetFileNameWithoutExtension( filenameFormatted ) + " {0}" + Path.GetExtension( filenameFormatted ) );
}
#endif
return saveDir;
}
private static Permission GetMediaFromGallery( MediaPickCallback callback, bool imageMode, string mime, string title, int maxSize )
{
Permission result = RequestPermission( true );
if( result == Permission.Granted && !IsMediaPickerBusy() )
{
#if !UNITY_EDITOR && UNITY_ANDROID
AJC.CallStatic( "PickMedia", Context, new NGMediaReceiveCallbackAndroid( callback, null ), imageMode, false, mime, title );
#elif !UNITY_EDITOR && UNITY_IOS
NGMediaReceiveCallbackiOS.Initialize( callback );
if( imageMode )
{
if( maxSize <= 0 )
maxSize = SystemInfo.maxTextureSize;
_NativeGallery_PickImage( IOSSelectedImagePath, maxSize );
}
else
_NativeGallery_PickVideo();
#else
if( callback != null )
callback( null );
#endif
}
return result;
}
private static Permission GetMultipleMediaFromGallery( MediaPickMultipleCallback callback, bool imageMode, string mime, string title, int maxSize )
{
Permission result = RequestPermission( true );
if( result == Permission.Granted && !IsMediaPickerBusy() )
{
if( CanSelectMultipleFilesFromGallery() )
{
#if !UNITY_EDITOR && UNITY_ANDROID
AJC.CallStatic( "PickMedia", Context, new NGMediaReceiveCallbackAndroid( null, callback ), imageMode, true, 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 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;
#if !UNITY_EDITOR && UNITY_IOS
if( orientation == ImageOrientation.Unknown ) // selected media is saved in correct orientation on iOS
orientation = ImageOrientation.Normal;
#endif
}
}
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,12 @@
fileFormatVersion: 2
guid: ce1403606c3629046a0147d3e705f7cc
timeCreated: 1498722610
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,79 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Coolape;
public class NativeGalleryUtl
{
public static IEnumerator TakeScreenshotAndSave(string album, string fileName)
{
yield return new WaitForEndOfFrame();
Texture2D ss = new Texture2D(Screen.width, Screen.height, TextureFormat.RGB24, false);
ss.ReadPixels(new Rect(0, 0, Screen.width, Screen.height), 0, 0);
ss.Apply();
// Save the screenshot to Gallery/Photos
Debug.Log("Permission result: " + NativeGallery.SaveImageToGallery(ss, album, fileName));
// To avoid memory leaks
Object.Destroy(ss);
}
public static void PickImage(object callback, int maxSize = -1)
{
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;
}
Utl.doCallback(callback, texture);
/*
// 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<Renderer>().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);
*/
}
}, "Select a PNG image", "image/png");
Debug.Log("Permission result: " + permission);
}
public static void PickVideo()
{
NativeGallery.Permission permission = NativeGallery.GetVideoFromGallery((path) =>
{
Debug.Log("Video path: " + path);
if (path != null)
{
// Play the selected video
Handheld.PlayFullScreenMovie("file://" + path);
}
}, "Select a video");
Debug.Log("Permission result: " + permission);
}
}

View File

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

View File

@@ -0,0 +1,87 @@
= 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
- change the value of PHOTO_LIBRARY_USAGE_DESCRIPTION in Plugins/NativeGallery/Editor/NGPostProcessBuild.cs (optional)
- 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. 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/filenameFormatted. On iOS, the image will be saved in the corresponding album
// filenameFormatted is string.Format'ed to avoid overwriting the same file on Android, if desired. If, for example, you want your images to be saved in a format like "My img 1.png", "My img 2.png" and etc., you can set the filenameFormatted as "My img {0}.png". {0} here is replaced with a unique number to avoid overwriting an existing file. If you don't use a {0} in your filenameFormatted parameter and a file with the same name does exist at that path, the file will be overwritten. On the other hand, a saved image is never overwritten on iOS
// 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 filenameFormatted, MediaSaveCallback callback = null );
NativeGallery.Permission NativeGallery.SaveImageToGallery( string existingMediaPath, string album, string filenameFormatted, MediaSaveCallback callback = null );
NativeGallery.Permission NativeGallery.SaveImageToGallery( Texture2D image, string album, string filenameFormatted, MediaSaveCallback callback = null );
NativeGallery.Permission NativeGallery.SaveVideoToGallery( byte[] mediaBytes, string album, string filenameFormatted, MediaSaveCallback callback = null );
NativeGallery.Permission NativeGallery.SaveVideoToGallery( string existingMediaPath, string album, string filenameFormatted, 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). On iOS, selected images will always be in PNG format and thus, this parameter has no effect on iOS
// maxSize: determines the maximum size of the returned image in pixels on iOS. A larger image will be down-scaled for better performance. If untouched, its value will be set to SystemInfo.maxTextureSize. Has no effect on Android
NativeGallery.Permission NativeGallery.GetImageFromGallery( MediaPickCallback callback, string title = "", string mime = "image/*", int maxSize = -1 );
NativeGallery.Permission NativeGallery.GetVideoFromGallery( MediaPickCallback callback, string title = "", string mime = "video/*" );
NativeGallery.Permission NativeGallery.GetImagesFromGallery( MediaPickMultipleCallback callback, string title = "", string mime = "image/*", int maxSize = -1 );
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 ////
// 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 from the specified image file in correct orientation and returns it. Returns null, if something goes wrong
NativeGallery.ImageProperties NativeGallery.GetImageProperties( string imagePath ): 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.VideoProperties NativeGallery.GetVideoProperties( string videoPath ): returns a VideoProperties instance that holds the width, height, duration (in milliseconds) and rotation information of a video file

View File

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

View File

@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 9c623599351a41a4c84c20f73c9d8976
folderAsset: yes
timeCreated: 1498722622
licenseType: Store
DefaultImporter:
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 )
{
if( callback != null )
{
callback( null );
callback = null;
}
IsBusy = false;
}
}
}
}
public void OnMediaReceived( string path )
{
if( string.IsNullOrEmpty( path ) )
path = null;
if( callback != null )
{
callback( path );
callback = null;
}
IsBusy = false;
}
}
}
#endif

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 71fb861c149c2d1428544c601e52a33c
timeCreated: 1519060539
licenseType: Store
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: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,593 @@
#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 *)imageSavePath;
+ (void)pickMediaSetMaxSize:(int)maxSize;
+ (int)isMediaPickerBusy;
+ (char *)getImageProperties:(NSString *)path;
+ (char *)getVideoProperties:(NSString *)path;
+ (char *)loadImageAtPath:(NSString *)path tempFilePath:(NSString *)tempFilePath maximumSize:(int)maximumSize;
@end
@implementation UNativeGallery
static NSString *pickedMediaSavePath;
static UIPopoverController *popup;
static UIImagePickerController *imagePicker;
static int pickMediaMaxSize = -1;
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 *)imageSavePath {
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];
pickedMediaSavePath = imageSavePath;
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 / 4, 0, 0 ) inView:rootViewController.view permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
}
}
+ (void)pickMediaSetMaxSize:(int)maxSize {
pickMediaMaxSize = maxSize;
}
+ (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]];
}
+ (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 {
NSArray *metadata = [self getImageMetadata:path];
int orientationInt = [metadata[2] intValue]; // 1: correct orientation, [1,8]: valid orientation range
if (( orientationInt <= 1 || orientationInt > 8 ) && [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 (scaledImage != image) {
[UIImagePNGRepresentation(scaledImage) writeToFile:tempFilePath atomically:YES];
return [self getCString:tempFilePath];
}
else
return [self getCString:path];
}
+ (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {
NSString *path;
if ([info[UIImagePickerControllerMediaType] isEqualToString:(NSString *)kUTTypeImage]) { // image picked
// Temporarily save image as PNG
UIImage *image = info[UIImagePickerControllerOriginalImage];
if (image == nil)
path = nil;
else {
[UIImagePNGRepresentation([self scaleImage:image maxSize:pickMediaMaxSize]) writeToFile:pickedMediaSavePath atomically:YES];
path = pickedMediaSavePath;
}
}
else { // video picked
NSURL *mediaUrl = info[UIImagePickerControllerMediaURL] ?: info[UIImagePickerControllerReferenceURL];
if (mediaUrl == nil)
path = nil;
else
path = [mediaUrl path];
}
popup = nil;
imagePicker = nil;
imagePickerState = 2;
UnitySendMessage("NGMediaReceiveCallbackiOS", "OnMediaReceived", [self getCString:path]);
[picker dismissViewControllerAnimated:NO completion:nil];
}
+ (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", "");
}
// 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, int maxSize) {
[UNativeGallery pickMediaSetMaxSize:maxSize];
[UNativeGallery pickMedia:YES savePath:[NSString stringWithUTF8String:imageSavePath]];
}
extern "C" void _NativeGallery_PickVideo() {
[UNativeGallery pickMedia:NO savePath:nil];
}
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_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: 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: