From 9c0a17dd63d6bb94cdcdebe14c20f145779d9af1 Mon Sep 17 00:00:00 2001 From: yasirkula Date: Thu, 13 Jul 2023 12:11:52 +0300 Subject: [PATCH] Added async variants of RequestPermission function that don't freeze the app unnecessarily --- .github/README.md | 13 +++++ .../NativeCamera/Android/NCCallbackHelper.cs | 13 +++-- .../Android/NCCameraCallbackAndroid.cs | 18 +------ .../Android/NCPermissionCallbackAndroid.cs | 17 +++++++ Plugins/NativeCamera/NativeCamera.cs | 31 +++++++++++- Plugins/NativeCamera/README.txt | 7 ++- .../iOS/NCPermissionCallbackiOS.cs | 34 +++++++++++++ .../iOS/NCPermissionCallbackiOS.cs.meta | 12 +++++ Plugins/NativeCamera/iOS/NativeCamera.mm | 48 +++++++++++++------ package.json | 2 +- 10 files changed, 157 insertions(+), 38 deletions(-) create mode 100644 Plugins/NativeCamera/iOS/NCPermissionCallbackiOS.cs create mode 100644 Plugins/NativeCamera/iOS/NCPermissionCallbackiOS.cs.meta diff --git a/.github/README.md b/.github/README.md index 753b4d2..438eedf 100644 --- a/.github/README.md +++ b/.github/README.md @@ -82,6 +82,11 @@ Beginning with *6.0 Marshmallow*, Android apps must request runtime permissions `NativeCamera.Permission NativeCamera.RequestPermission( bool isPicturePermission )`: requests permission to access the camera from the user and returns the result. It is recommended to show a brief explanation before asking the permission so that user understands why the permission is needed and doesn't click Deny or worse, "Don't ask again". Note that TakePicture and RecordVideo functions call RequestPermission internally and execute only if the permission is granted (the result of RequestPermission is then returned). +`void NativeCamera.RequestPermissionAsync( PermissionCallback callback, bool isPicturePermission )`: Asynchronous variant of *RequestPermission*. Unlike RequestPermission, this function doesn't freeze the app unnecessarily before the permission dialog is displayed. So it's recommended to call this function instead. +- **PermissionCallback** takes `NativeCamera.Permission permission` parameter + +`Task NativeCamera.RequestPermissionAsync( bool isPicturePermission )`: Another asynchronous variant of *RequestPermission* (requires Unity 2018.4 or later). + `NativeCamera.OpenSettings()`: opens the settings for this app, from where the user can manually grant permission in case current permission state is *Permission.Denied* (Android requires *Storage* and, if declared in AndroidManifest, *Camera* permissions; iOS requires *Camera* permission). `bool NativeCamera.CanOpenSettings()`: on iOS versions prior to 8.0, opening settings from within the app is not possible and in this case, this function returns *false*. Otherwise, it returns *true*. @@ -137,6 +142,14 @@ void Update() } } +// Example code doesn't use this function but it is here for reference. It's recommended to ask for permissions manually using the +// RequestPermissionAsync methods prior to calling NativeCamera functions +private async void RequestPermissionAsynchronously( bool isPicturePermission ) +{ + NativeCamera.Permission permission = await NativeCamera.RequestPermissionAsync( isPicturePermission ); + Debug.Log( "Permission result: " + permission ); +} + private void TakePicture( int maxSize ) { NativeCamera.Permission permission = NativeCamera.TakePicture( ( path ) => diff --git a/Plugins/NativeCamera/Android/NCCallbackHelper.cs b/Plugins/NativeCamera/Android/NCCallbackHelper.cs index f2adbe9..9f7a173 100644 --- a/Plugins/NativeCamera/Android/NCCallbackHelper.cs +++ b/Plugins/NativeCamera/Android/NCCallbackHelper.cs @@ -16,9 +16,16 @@ private void Update() { if( mainThreadAction != null ) { - System.Action temp = mainThreadAction; - mainThreadAction = null; - temp(); + try + { + System.Action temp = mainThreadAction; + mainThreadAction = null; + temp(); + } + finally + { + Destroy( gameObject ); + } } } diff --git a/Plugins/NativeCamera/Android/NCCameraCallbackAndroid.cs b/Plugins/NativeCamera/Android/NCCameraCallbackAndroid.cs index aa3c9b5..c28d0b3 100644 --- a/Plugins/NativeCamera/Android/NCCameraCallbackAndroid.cs +++ b/Plugins/NativeCamera/Android/NCCameraCallbackAndroid.cs @@ -16,23 +16,7 @@ public NCCameraCallbackAndroid( NativeCamera.CameraCallback callback ) : base( " public void OnMediaReceived( string path ) { - callbackHelper.CallOnMainThread( () => MediaReceiveCallback( path ) ); - } - - private void MediaReceiveCallback( string path ) - { - if( string.IsNullOrEmpty( path ) ) - path = null; - - try - { - if( callback != null ) - callback( path ); - } - finally - { - Object.Destroy( callbackHelper.gameObject ); - } + callbackHelper.CallOnMainThread( () => callback( !string.IsNullOrEmpty( path ) ? path : null ) ); } } } diff --git a/Plugins/NativeCamera/Android/NCPermissionCallbackAndroid.cs b/Plugins/NativeCamera/Android/NCPermissionCallbackAndroid.cs index f618bf4..091fee0 100644 --- a/Plugins/NativeCamera/Android/NCPermissionCallbackAndroid.cs +++ b/Plugins/NativeCamera/Android/NCPermissionCallbackAndroid.cs @@ -25,5 +25,22 @@ public void OnPermissionResult( int result ) } } } + + public class NCPermissionCallbackAsyncAndroid : AndroidJavaProxy + { + private readonly NativeCamera.PermissionCallback callback; + private readonly NCCallbackHelper callbackHelper; + + public NCPermissionCallbackAsyncAndroid( NativeCamera.PermissionCallback callback ) : base( "com.yasirkula.unity.NativeCameraPermissionReceiver" ) + { + this.callback = callback; + callbackHelper = new GameObject( "NCCallbackHelper" ).AddComponent(); + } + + public void OnPermissionResult( int result ) + { + callbackHelper.CallOnMainThread( () => callback( (NativeCamera.Permission) result ) ); + } + } } #endif \ No newline at end of file diff --git a/Plugins/NativeCamera/NativeCamera.cs b/Plugins/NativeCamera/NativeCamera.cs index f6cdd09..ae62e14 100644 --- a/Plugins/NativeCamera/NativeCamera.cs +++ b/Plugins/NativeCamera/NativeCamera.cs @@ -53,6 +53,7 @@ public enum PreferredCamera { Default = -1, Rear = 0, Front = 1 } // EXIF orientation: http://sylvana.net/jpegcrop/exif_orientation.html (indices are reordered) public enum ImageOrientation { Unknown = -1, Normal = 0, Rotate90 = 1, Rotate180 = 2, Rotate270 = 3, FlipHorizontal = 4, Transpose = 5, FlipVertical = 6, Transverse = 7 }; + public delegate void PermissionCallback( Permission permission ); public delegate void CameraCallback( string path ); #region Platform Specific Elements @@ -90,7 +91,7 @@ private static AndroidJavaObject Context private static extern int _NativeCamera_CheckPermission(); [System.Runtime.InteropServices.DllImport( "__Internal" )] - private static extern int _NativeCamera_RequestPermission(); + private static extern int _NativeCamera_RequestPermission( int asyncMode ); [System.Runtime.InteropServices.DllImport( "__Internal" )] private static extern int _NativeCamera_CanOpenSettings(); @@ -173,6 +174,10 @@ public static Permission CheckPermission( bool isPicturePermission ) public static Permission RequestPermission( bool isPicturePermission ) { + // Don't block the main thread if the permission is already granted + if( CheckPermission( isPicturePermission ) == Permission.Granted ) + return Permission.Granted; + #if !UNITY_EDITOR && UNITY_ANDROID object threadLock = new object(); lock( threadLock ) @@ -193,12 +198,34 @@ public static Permission RequestPermission( bool isPicturePermission ) return (Permission) nativeCallback.Result; } #elif !UNITY_EDITOR && UNITY_IOS - return (Permission) _NativeCamera_RequestPermission(); + return (Permission) _NativeCamera_RequestPermission( 0 ); #else return Permission.Granted; #endif } + public static void RequestPermissionAsync( PermissionCallback callback, bool isPicturePermission ) + { +#if !UNITY_EDITOR && UNITY_ANDROID + NCPermissionCallbackAsyncAndroid nativeCallback = new NCPermissionCallbackAsyncAndroid( callback ); + AJC.CallStatic( "RequestPermission", Context, nativeCallback, isPicturePermission, (int) Permission.ShouldAsk ); +#elif !UNITY_EDITOR && UNITY_IOS + NCPermissionCallbackiOS.Initialize( callback ); + _NativeCamera_RequestPermission( 1 ); +#else + callback( Permission.Granted ); +#endif + } + +#if UNITY_2018_4_OR_NEWER && !NATIVE_CAMERA_DISABLE_ASYNC_FUNCTIONS + public static Task RequestPermissionAsync( bool isPicturePermission ) + { + TaskCompletionSource tcs = new TaskCompletionSource(); + RequestPermissionAsync( ( permission ) => tcs.SetResult( permission ), isPicturePermission ); + return tcs.Task; + } +#endif + public static bool CanOpenSettings() { #if !UNITY_EDITOR && UNITY_IOS diff --git a/Plugins/NativeCamera/README.txt b/Plugins/NativeCamera/README.txt index 6b27ab4..c5c57de 100644 --- a/Plugins/NativeCamera/README.txt +++ b/Plugins/NativeCamera/README.txt @@ -1,4 +1,4 @@ -= Native Camera for Android & iOS (v1.3.9) = += Native Camera for Android & iOS (v1.4.0) = Online documentation & example code available at: https://github.com/yasirkula/UnityNativeCamera E-mail: yasirkula@gmail.com @@ -38,6 +38,7 @@ enum NativeCamera.Permission { Denied = 0, Granted = 1, ShouldAsk = 2 }; enum NativeCamera.Quality { Default = -1, Low = 0, Medium = 1, High = 2 }; enum NativeCamera.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 PermissionCallback( NativeCamera.Permission permission ); delegate void CameraCallback( string path ); //// Accessing Camera //// @@ -66,6 +67,10 @@ bool NativeCamera.IsCameraBusy(); // returns true if the camera is currently ope NativeCamera.Permission NativeCamera.CheckPermission( bool isPicturePermission ); NativeCamera.Permission NativeCamera.RequestPermission( bool isPicturePermission ); +// Asynchronous variants of RequestPermission. Unlike RequestPermission, these functions don't freeze the app unnecessarily before the permission dialog is displayed. So it's recommended to call these functions instead +void NativeCamera.RequestPermissionAsync( PermissionCallback callback, PermissionType permissionType, MediaType mediaTypes ); +Task NativeCamera.RequestPermissionAsync( PermissionType permissionType, MediaType mediaTypes ); + // If permission state is Permission.Denied, user must grant the necessary permission(s) manually from the Settings (Android requires Storage and, if declared in AndroidManifest, Camera permissions; iOS requires Camera permission). These functions help you open the Settings directly from within the app void NativeCamera.OpenSettings(); bool NativeCamera.CanOpenSettings(); diff --git a/Plugins/NativeCamera/iOS/NCPermissionCallbackiOS.cs b/Plugins/NativeCamera/iOS/NCPermissionCallbackiOS.cs new file mode 100644 index 0000000..3bcd980 --- /dev/null +++ b/Plugins/NativeCamera/iOS/NCPermissionCallbackiOS.cs @@ -0,0 +1,34 @@ +#if UNITY_EDITOR || UNITY_IOS +using UnityEngine; + +namespace NativeCameraNamespace +{ + public class NCPermissionCallbackiOS : MonoBehaviour + { + private static NCPermissionCallbackiOS instance; + private NativeCamera.PermissionCallback callback; + + public static void Initialize( NativeCamera.PermissionCallback callback ) + { + if( instance == null ) + { + instance = new GameObject( "NCPermissionCallbackiOS" ).AddComponent(); + DontDestroyOnLoad( instance.gameObject ); + } + else if( instance.callback != null ) + instance.callback( NativeCamera.Permission.ShouldAsk ); + + instance.callback = callback; + } + + public void OnPermissionRequested( string message ) + { + NativeCamera.PermissionCallback _callback = callback; + callback = null; + + if( _callback != null ) + _callback( (NativeCamera.Permission) int.Parse( message ) ); + } + } +} +#endif \ No newline at end of file diff --git a/Plugins/NativeCamera/iOS/NCPermissionCallbackiOS.cs.meta b/Plugins/NativeCamera/iOS/NCPermissionCallbackiOS.cs.meta new file mode 100644 index 0000000..4f69ad2 --- /dev/null +++ b/Plugins/NativeCamera/iOS/NCPermissionCallbackiOS.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 1efd0cf9fb7457142b76fb1bc8672e8d +timeCreated: 1519060539 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Plugins/NativeCamera/iOS/NativeCamera.mm b/Plugins/NativeCamera/iOS/NativeCamera.mm index ca28511..592ff0e 100644 --- a/Plugins/NativeCamera/iOS/NativeCamera.mm +++ b/Plugins/NativeCamera/iOS/NativeCamera.mm @@ -14,7 +14,7 @@ @interface UNativeCamera:NSObject + (int)checkPermission; -+ (int)requestPermission; ++ (int)requestPermission:(BOOL)asyncMode; + (int)canOpenSettings; + (void)openSettings; + (int)hasCamera; @@ -55,7 +55,16 @@ + (int)checkPermission } // Credit: https://stackoverflow.com/a/20464727/2373034 -+ (int)requestPermission ++ (int)requestPermission:(BOOL)asyncMode +{ + int result = [self requestPermissionInternal:asyncMode]; + if( asyncMode && result >= 0 ) // Result returned immediately, forward it + UnitySendMessage( "NCPermissionCallbackiOS", "OnPermissionRequested", [self getCString:[NSString stringWithFormat:@"%d", result]] ); + + return result; +} + ++ (int)requestPermissionInternal:(BOOL)asyncMode { if( CHECK_IOS_VERSION( @"7.0" ) ) { @@ -64,17 +73,28 @@ + (int)requestPermission return 1; else if( status == AVAuthorizationStatusNotDetermined ) { - __block BOOL authorized = NO; - - dispatch_semaphore_t sema = dispatch_semaphore_create( 0 ); - [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^( BOOL granted ) + if( asyncMode ) { - authorized = granted; - dispatch_semaphore_signal( sema ); - }]; - dispatch_semaphore_wait( sema, DISPATCH_TIME_FOREVER ); - - return authorized ? 1 : 0; + [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^( BOOL granted ) + { + UnitySendMessage( "NCPermissionCallbackiOS", "OnPermissionRequested", granted ? "1" : "0" ); + }]; + + return -1; + } + else + { + __block BOOL authorized = NO; + dispatch_semaphore_t sema = dispatch_semaphore_create( 0 ); + [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^( BOOL granted ) + { + authorized = granted; + dispatch_semaphore_signal( sema ); + }]; + dispatch_semaphore_wait( sema, DISPATCH_TIME_FOREVER ); + + return authorized ? 1 : 0; + } } else return 0; @@ -552,9 +572,9 @@ + (char *)getCString:(NSString *)source return [UNativeCamera checkPermission]; } -extern "C" int _NativeCamera_RequestPermission() +extern "C" int _NativeCamera_RequestPermission( int asyncMode ) { - return [UNativeCamera requestPermission]; + return [UNativeCamera requestPermission:( asyncMode == 1 )]; } extern "C" int _NativeCamera_CanOpenSettings() diff --git a/package.json b/package.json index 0b7454b..368114c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "com.yasirkula.nativecamera", "displayName": "Native Camera", - "version": "1.3.9", + "version": "1.4.0", "documentationUrl": "https://github.com/yasirkula/UnityNativeCamera", "changelogUrl": "https://github.com/yasirkula/UnityNativeCamera/releases", "licensesUrl": "https://github.com/yasirkula/UnityNativeCamera/blob/master/LICENSE.txt",