diff --git a/Src/Authorizer.cs b/Src/Authorizer.cs
index 0bd48da..3c7c893 100644
--- a/Src/Authorizer.cs
+++ b/Src/Authorizer.cs
@@ -65,7 +65,7 @@ public string CurrentEmotivId
public Authorizer()
{
- _ctxClient.WSConnectDone += OnWSConnectDone;
+ _ctxClient.CortexConnectionStared += OnCortexConnectionStared;
_ctxClient.GetUserLoginDone += OnGetUserLoginDone;
_ctxClient.UserLoginNotify += OnUserLoginNotify; // inform user loggin
_ctxClient.UserLogoutNotify += OnUserLogoutNotify; // inform user log out
@@ -222,10 +222,10 @@ private void OnRefreshTokenOK(object sender, string cortexToken)
SaveToken(tokenInfo);
// get license information
- _ctxClient.GetLicenseInfo(cortexToken);
+ _ctxClient.GetLicenseInfo();
}
- private void OnWSConnectDone(object sender, bool isConnected)
+ private void OnCortexConnectionStared(object sender, bool isConnected)
{
if (isConnected) {
#if UNITY_ANDROID || UNITY_IOS
@@ -271,7 +271,7 @@ private void OnAuthorizedOK(object sender, string cortexToken)
Authorizer.SaveToken(tokenInfo);
// get license information
- _ctxClient.GetLicenseInfo(cortexToken);
+ _ctxClient.GetLicenseInfo();
} else {
AuthorizedFailed(this, cortexToken);
UnityEngine.Debug.Log("Invalid Token.");
diff --git a/Src/BCIGameItf.cs b/Src/BCIGameItf.cs
index 40a4847..510ded7 100644
--- a/Src/BCIGameItf.cs
+++ b/Src/BCIGameItf.cs
@@ -41,11 +41,6 @@ public void LoginWithAuthenticationCode(string code)
emotivUnityItf.LoginWithAuthenticationCode(code);
}
- public void AcceptEulaAndPrivacyPolicy()
- {
- emotivUnityItf.AcceptEulaAndPrivacyPolicy();
- }
-
///
/// Get detected headsets. Returns a list of detected headsets.
///
@@ -173,15 +168,6 @@ public async Task ProcessCallback(string args) {
}
#endif
- #if USE_EMBEDDED_LIB || UNITY_ANDROID || UNITY_IOS
- ///
- /// Authenticate with Emotiv. It will open system browser to login and get the authentication code.
- ///
- public async Task AuthenticateAsync()
- {
- await emotivUnityItf.AuthenticateAsync();
- }
- #endif
///
/// Initialize and start the application. It should be called when the app has granted permissions: bluetooth, location, write external storage.
@@ -390,12 +376,6 @@ public void EraseDataForMCTrainingAction(string action = DEFAULT_MC_ACTION)
emotivUnityItf.EraseMCTraining(action);
}
- // logout
- public void Logout()
- {
- emotivUnityItf.Logout();
- }
-
///
/// Query the dates having consumer data within a specified date range.
///
@@ -414,16 +394,6 @@ public void QueryDayDetailOfConsumerData(DateTime date) {
emotivUnityItf.QueryDayDetailOfConsumerData(date);
}
- public void OpenURL(string url)
- {
- emotivUnityItf.OpenURL(url);
- }
-
- public bool IsWebViewOpened()
- {
- return emotivUnityItf.IsWebViewOpened;
- }
-
public string LoadedProfilePlayer() {
return emotivUnityItf.LoadedProfileName;
}
diff --git a/Src/CortexClient.cs b/Src/CortexClient.cs
index 32412e5..1855dc8 100644
--- a/Src/CortexClient.cs
+++ b/Src/CortexClient.cs
@@ -26,6 +26,7 @@
using System;
using System.Threading;
using Newtonsoft.Json.Linq;
+using Emotiv.Cortex.Models;
using System.Collections.Generic;
using System.Collections;
@@ -49,7 +50,7 @@ public abstract class CortexClient
public AutoResetEvent m_MessageReceiveEvent = new AutoResetEvent(false);
public AutoResetEvent m_OpenedEvent = new AutoResetEvent(false);
- public event EventHandler WSConnectDone;
+ public event EventHandler CortexConnectionStared;
public event EventHandler ErrorMsgReceived;
public event EventHandler StreamDataReceived;
public event EventHandler> QueryHeadsetOK;
@@ -61,11 +62,13 @@ public abstract class CortexClient
public event EventHandler AccessRightGrantedDone;
public event EventHandler AuthorizeOK;
public event EventHandler GetUserLoginDone;
+ public event EventHandler LoginDone;
public event EventHandler EULAAccepted;
public event EventHandler EULANotAccepted; // return cortexToken if user has not accept eula to proceed next step
public event EventHandler UserLoginNotify;
public event EventHandler UserLogoutNotify;
public event EventHandler GetLicenseInfoDone;
+ public event EventHandler<(CortexErrorCode error, License data)> GetLicenseInfoResult;
public event EventHandler CreateSessionOK;
public event EventHandler UpdateSessionOK;
public event EventHandler SubscribeDataDone;
@@ -103,6 +106,7 @@ public abstract class CortexClient
public event EventHandler> QueryDayDetailOfConsumerDataDone;
public event EventHandler ExportRecordsFinished;
public event EventHandler DataPostProcessingFinished;
+ public string CurrentCortexToken { get; internal set; } = string.Empty;
public virtual void Init(object context = null) {}
@@ -148,9 +152,9 @@ protected string PrepareRequest(string method, JObject param, bool hasParam = tr
return request.ToString();
}
- public void OnWSConnected(bool isConnected)
+ public void OnCortexConnectionStared(bool isConnected)
{
- WSConnectDone(this, isConnected);
+ CortexConnectionStared(this, isConnected);
}
///
@@ -185,6 +189,11 @@ public void OnMessageReceived(string receievedMsg)
UnityEngine.Debug.Log("An error received: " + messageError);
//Send Error message event
ErrorMsgReceived(this, new ErrorMsgEventArgs(code, messageError, method));
+
+ if (method == "getLicenseInfo")
+ {
+ GetLicenseInfoResult?.Invoke(this, (CortexErrorCode.UnknownError, null));
+ }
} else {
// handle response
@@ -302,11 +311,12 @@ private void HandleResponse(string method, JToken data)
loginData.EmotivId = data["username"].ToString();
String message = data["message"].ToString();
UnityEngine.Debug.Log("login message: " + message);
- GetUserLoginDone(this, loginData);
+ LoginDone(this, loginData);
}
else if (method == "logout")
{
String message = data["message"].ToString();
+ CurrentCortexToken = string.Empty;
UserLogoutNotify(this, message);
// get user login info
GetUserLogin();
@@ -324,12 +334,14 @@ private void HandleResponse(string method, JToken data)
else if (method == "generateNewToken")
{
string cortexToken = data["cortexToken"].ToString();
+ CurrentCortexToken = cortexToken;
RefreshTokenOK(this, cortexToken);
}
else if (method == "getLicenseInfo")
{
License lic = new License(data["license"]);
- GetLicenseInfoDone(this, lic);
+ // GetLicenseInfoDone(this, lic);
+ GetLicenseInfoResult?.Invoke(this, (CortexErrorCode.OK, lic));
}
else if (method == "getUserInformation")
{
@@ -338,6 +350,7 @@ private void HandleResponse(string method, JToken data)
else if (method == "authorize")
{
string token = (string)data["cortexToken"];
+ CurrentCortexToken = token;
if (data["warning"] != null)
{
JObject warning = (JObject)data["warning"];
@@ -658,6 +671,16 @@ public void LoginWithAuthenticationCode(string code)
}
// accept eula
+ public void AcceptEulaAndPrivacyPolicy()
+ {
+ if (string.IsNullOrEmpty(CurrentCortexToken))
+ {
+ UnityEngine.Debug.LogWarning("AcceptEulaAndPrivacyPolicy requested but no cortex token is available.");
+ return;
+ }
+ AcceptEulaAndPrivacyPolicy(CurrentCortexToken);
+ }
+
public void AcceptEulaAndPrivacyPolicy(string cortexToken)
{
JObject param = new JObject(
@@ -667,10 +690,17 @@ public void AcceptEulaAndPrivacyPolicy(string cortexToken)
}
// get license information
- public void GetLicenseInfo(string cortexToken)
+ public void GetLicenseInfo()
{
+ if (string.IsNullOrEmpty(CurrentCortexToken))
+ {
+ UnityEngine.Debug.LogWarning("GetLicenseInfo requested but no cortex token is available.");
+ GetLicenseInfoResult?.Invoke(this, (CortexErrorCode.UnknownError, null));
+ return;
+ }
+
JObject param = new JObject(
- new JProperty("cortexToken", cortexToken)
+ new JProperty("cortexToken", CurrentCortexToken)
);
SendTextMessage(param, "getLicenseInfo", true);
}
diff --git a/Src/EmbeddedCortexClient.cs b/Src/EmbeddedCortexClient.cs
index ab1f0b7..c542479 100644
--- a/Src/EmbeddedCortexClient.cs
+++ b/Src/EmbeddedCortexClient.cs
@@ -37,7 +37,7 @@ public class CortexLibInterfaceProxy : AndroidJavaProxy
{
public CortexLibInterfaceProxy() : base("com.emotiv.unityplugin.CortexConnectionInterface") { }
void onReceivedMessage(String msg) => EmbeddedCortexClient.Instance.OnMessageReceived(msg);
- void onCortexStarted() { Debug.Log("Cortex Lib Started"); EmbeddedCortexClient.Instance.OnWSConnected(true); }
+ void onCortexStarted() { Debug.Log("Cortex Lib Started"); EmbeddedCortexClient.Instance.OnCortexConnectionStared(true); }
}
public class CortexLogHandler : AndroidJavaProxy
{
@@ -75,7 +75,7 @@ public class CortexIOSHandler
[AOT.MonoPInvokeCallback(typeof(MessageCallback))]
private static void OnMessageReceived(string message) => EmbeddedCortexClient.Instance.OnMessageReceived(message);
[AOT.MonoPInvokeCallback(typeof(StartedCallback))]
- private static void OnCortexLibIosStarted() { Debug.Log("OnCortexLibIosStarted"); EmbeddedCortexClient.Instance.OnWSConnected(true); }
+ private static void OnCortexLibIosStarted() { Debug.Log("OnCortexLibIosStarted"); EmbeddedCortexClient.Instance.OnCortexConnectionStared(true); }
public static void RegisterCallback() { RegisterUnityResponseCallback(OnMessageReceived); RegisterUnityStartedCallback(OnCortexLibIosStarted); }
}
#endif
@@ -124,7 +124,7 @@ public override void Init(object context = null)
}
private static void OnMessageReceivedStatic(string message) => EmbeddedCortexClient.Instance.OnMessageReceived(message);
- private static void OnWSConnectedStatic() => EmbeddedCortexClient.Instance.OnWSConnected(true);
+ private static void OnCortexConnectionStaredStatic() => EmbeddedCortexClient.Instance.OnCortexConnectionStared(true);
#if USE_EMBEDDED_LIB
private void CortexStarted(object? sender, bool e)
diff --git a/Src/EmotivUnityItf.cs b/Src/EmotivUnityItf.cs
index 3b1b805..6771f9c 100644
--- a/Src/EmotivUnityItf.cs
+++ b/Src/EmotivUnityItf.cs
@@ -7,16 +7,6 @@
using System.Threading.Tasks;
using UnityEngine;
-#if USE_EMBEDDED_LIB || UNITY_ANDROID || UNITY_IOS
-using Cdm.Authentication.Browser;
-using Cdm.Authentication.OAuth2;
-using System.Threading;
-using System.Security.Cryptography;
-using System.Text;
-using Cdm.Authentication.Clients;
-using Newtonsoft.Json;
-#endif
-
namespace EmotivUnityPlugin
{
@@ -70,7 +60,6 @@ public class EmotivUnityItf
private string _messageLog = "";
- private bool _isWebViewOpened = false;
private List _mentalCommandActionSensitivity = new List();
// trained signature actions
@@ -102,7 +91,6 @@ public class EmotivUnityItf
public List MentalCommandActionSensitivity { get => _mentalCommandActionSensitivity; set => _mentalCommandActionSensitivity = value; }
public List DatesHavingConsumerData { get => _datesHavingConsumerData; set => _datesHavingConsumerData = value; }
public List MentalStateDatas { get => _mentalStateDatas; set => _mentalStateDatas = value; }
- public bool IsWebViewOpened { get => _isWebViewOpened; set => _isWebViewOpened = value; }
public string LoadedProfileName { get => _loadedProfileName; set => _loadedProfileName = value; }
public string WorkingHeadsetId { get => _workingHeadsetId; set => _workingHeadsetId = value; }
public Record RecentRecord { get => _recentRecord; set => _recentRecord = value; }
@@ -121,15 +109,6 @@ public string GetCurrentEmotivId()
}
-#if USE_EMBEDDED_LIB || UNITY_ANDROID || UNITY_IOS
- private CrossPlatformBrowser _crossPlatformBrowser;
- private AuthenticationSession _authenticationSession;
- private CancellationTokenSource _cancellationTokenSource;
-
- private static readonly char[] HEX_ARRAY = "0123456789abcdef".ToCharArray();
-
- #endif
-
#if USE_EMBEDDED_LIB && UNITY_STANDALONE_WIN && !UNITY_EDITOR
public async Task ProcessCallback(string args)
{
@@ -205,7 +184,7 @@ public ConnectToCortexStates GetConnectToCortexState()
/// The path to Emotiv Launcher file path (optional). Only for desktop version and work with Cortex Service.
public void Init(string clientId, string clientSecret, string appName,
bool allowSaveLogToFile = true, bool isDataBufferUsing = true,
- string appUrl = "", string providerName = "", string emotivAppsPath = "")
+ string appUrl = "wss://localhost:6868", string providerName = "", string emotivAppsPath = "")
{
if (string.IsNullOrEmpty(clientId) || string.IsNullOrEmpty(clientSecret))
{
@@ -225,11 +204,6 @@ public void Init(string clientId, string clientSecret, string appName,
// init logger
MyLogger.Instance.Init(appName, allowSaveLogToFile);
- // init authentication for Android and Embedded Cortex Desktop
- #if UNITY_ANDROID || USE_EMBEDDED_LIB || UNITY_IOS
- InitForAuthentication(clientId, clientSecret);
- #endif
-
_dsManager.IsDataBufferUsing = isDataBufferUsing;
// init bcitraining
_bciTraining.Init();
@@ -302,10 +276,6 @@ public void Start(object context = null)
///
public void Stop()
{
- #if USE_EMBEDDED_LIB
- _cancellationTokenSource?.Cancel();
- _authenticationSession?.Dispose();
- #endif
#if UNITY_ANDROID || UNITY_IOS
UniWebViewManager.Instance?.Cleanup();
#endif
@@ -1076,7 +1046,7 @@ private void OnMentalCommandReceived(object sender, MentalCommandEventArgs data)
{
string dataText = "com data: " + data.Act + ", power: " + data.Pow.ToString() + ", time " + data.Time.ToString();
// print out data to console
- UnityEngine.Debug.Log(dataText);
+ //UnityEngine.Debug.Log(dataText);
LatestMentalCommand.act = data.Act;
LatestMentalCommand.pow = data.Pow;
}
@@ -1285,147 +1255,5 @@ public void ResetProfileAndTrainingData()
_desiredErasingProfiles.Clear();
}
- public void OpenURL(string url)
- {
- #if UNITY_ANDROID || UNITY_IOS
- _isWebViewOpened = true;
- UniWebViewManager.Instance.OpenURL(
- url,
- onClosed: (isClosed) => {
- Debug.Log($"UniWebView closed! isClosed: {isClosed}");
- _isWebViewOpened = false;
-
- }
- );
- #else
- Application.OpenURL(url);
- #endif
- }
-
- #if USE_EMBEDDED_LIB || UNITY_ANDROID || UNITY_IOS
- private string BytesToHex(byte[] bytes)
- {
- char[] hexChars = new char[bytes.Length * 2];
- for (int j = 0; j < bytes.Length; ++j)
- {
- int v = bytes[j] & 0xFF;
- hexChars[j * 2] = HEX_ARRAY[v >> 4];
- hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
- }
- return new string(hexChars);
- }
-
- private string Md5(string s)
- {
- try
- {
- using (var md5 = MD5.Create())
- {
- byte[] inputBytes = Encoding.UTF8.GetBytes(s);
- byte[] hashBytes = md5.ComputeHash(inputBytes);
- return BytesToHex(hashBytes);
- }
- }
- catch (Exception e)
- {
- Debug.LogError(e);
- return string.Empty;
- }
- }
-
- private void InitForAuthentication(string clientId, string clientSecret)
- {
- string server = "";
- #if DEV_SERVER
- UnityEngine.Debug.Log("Development build detected. Using development server.");
- server = "cerebrum-dev.emotivcloud.com";
-#else
- UnityEngine.Debug.Log("Production build detected. Using production server.");
- server = "cerebrum.emotivcloud.com";
-#endif
- string hash = Md5(clientId);
- string prefixRedirectUrl = "emotiv-" + hash;
- string redirectUrl = prefixRedirectUrl + "://authorize";
- string serverUrl = $"https://{server}";
- #if UNITY_ANDROID || UNITY_IOS
- string authorizationUrl = $"https://{server}/api/oauth/authorize/?response_type=code" +
- $"&client_id={Uri.EscapeDataString(clientId)}" +
- $"&redirect_uri={redirectUrl}" + $"&hide_signup=1&hide_social_signin=1";
- UniWebViewManager.Instance.Init(
- authorizationUrl,
- prefixRedirectUrl
- );
- #else
- _crossPlatformBrowser = new CrossPlatformBrowser();
- _crossPlatformBrowser.platformBrowsers.Add(RuntimePlatform.WindowsEditor, new WindowsSystemBrowser());
- _crossPlatformBrowser.platformBrowsers.Add(RuntimePlatform.WindowsPlayer, new WindowsSystemBrowser());
- _crossPlatformBrowser.platformBrowsers.Add(RuntimePlatform.OSXEditor, new DeepLinkBrowser());
- _crossPlatformBrowser.platformBrowsers.Add(RuntimePlatform.OSXPlayer, new DeepLinkBrowser());
-
-#if UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN
- // Deep linking is not supported on Windows (except UWP), so RegistryConfig is used to handle this case.
- new RegistryConfig(prefixRedirectUrl).Configure();
-#endif
-
- var configuration = new AuthorizationCodeFlow.Configuration()
- {
- clientId = clientId,
- clientSecret = clientSecret,
- redirectUri = redirectUrl,
- scope = ""
- };
- var auth = new MockServerAuth(configuration, serverUrl);
- _authenticationSession = new AuthenticationSession(auth, _crossPlatformBrowser);
- _authenticationSession.loginTimeout = TimeSpan.FromSeconds(600);
- #endif
- }
- public async Task AuthenticateAsync()
- {
- #if UNITY_ANDROID || UNITY_IOS
- _isWebViewOpened = true;
- UniWebViewManager.Instance.StartAuthorization(
- onSuccess: (authCode) => {
- Debug.Log($"UniWebView Authorization succeeded! Starting login with auth code");
- LoginWithAuthenticationCode(authCode);
- _isWebViewOpened = false;
- },
- onError: (errorCode, errorMessage) => {
- Debug.LogError($"Authorization failed! Error {errorCode}: {errorMessage}");
- _isWebViewOpened = false;
-
- }
- );
- #else
- if (_authenticationSession != null)
- {
- _cancellationTokenSource?.Dispose();
- _cancellationTokenSource = new CancellationTokenSource();
- try
- {
- MessageLog = "Starting authentication...";
- var accessTokenResponse =
- await _authenticationSession.AuthenticateAsync(_cancellationTokenSource.Token);
-
- LoginWithAuthenticationCode(accessTokenResponse.accessToken);
- }
- catch (AuthorizationCodeRequestException ex)
- {
- Debug.LogError($"{nameof(AuthorizationCodeRequestException)} " +
- $"error: {ex.error.code}, description: {ex.error.description}, uri: {ex.error.uri}");
- }
- catch (AccessTokenRequestException ex)
- {
- Debug.LogError($"{nameof(AccessTokenRequestException)} " +
- $"error: {ex.error.code}, description: {ex.error.description}, uri: {ex.error.uri}");
- }
- catch (Exception ex)
- {
- Debug.LogError( "Exception " + ex.Message);
- }
- }
- #endif
- }
- #endif
-
}
}
diff --git a/Src/Runtime/Models/CortexErrorCode.cs b/Src/Runtime/Models/CortexErrorCode.cs
new file mode 100644
index 0000000..ac39fed
--- /dev/null
+++ b/Src/Runtime/Models/CortexErrorCode.cs
@@ -0,0 +1,13 @@
+namespace Emotiv.Cortex.Models
+{
+ public enum CortexErrorCode
+ {
+ OK = 0,
+ PermissionsDenied = 2, // Indicates that the requested operation failed because the required permissions were not granted.
+ CortexConnectionError = 3, // Indicates that the connection to Cortex started unsuccessfully.
+ NoEULAAccepted = 4, // Indicates that the user has not accepted the End User License Agreement (EULA). Only for Cortex v3. Must be removed at Cortex v5
+ AuthorizationFailed = 5, // Indicates that the authorization process failed overall.
+ LicenseError = 6, // Indicates that there is an issue with the license (e.g., invalid or expired license).
+ UnknownError
+ }
+}
diff --git a/Src/Runtime/SDK/Auth/AuthService.cs b/Src/Runtime/SDK/Auth/AuthService.cs
new file mode 100644
index 0000000..e87e6f4
--- /dev/null
+++ b/Src/Runtime/SDK/Auth/AuthService.cs
@@ -0,0 +1,530 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Emotiv.Cortex.Models;
+using EmotivUnityPlugin;
+using UnityEngine;
+#if USE_EMBEDDED_LIB || UNITY_ANDROID || UNITY_IOS
+using Cdm.Authentication.Browser;
+using Cdm.Authentication.OAuth2;
+using System.Threading;
+using System.Security.Cryptography;
+using System.Text;
+using Cdm.Authentication.Clients;
+#endif
+#if UNITY_ANDROID
+using UnityEngine.Android;
+#endif
+
+namespace Emotiv.Cortex.Service
+{
+ public class AuthService : IAuthService
+ {
+ public static AuthService Instance { get; } = new AuthService();
+
+ private readonly CortexClient _client;
+ private UserDataInfo _loggedInUser = new UserDataInfo();
+ private bool _isInitialized;
+
+#if USE_EMBEDDED_LIB || UNITY_ANDROID || UNITY_IOS
+ private CrossPlatformBrowser _crossPlatformBrowser;
+ private AuthenticationSession _authenticationSession;
+ private CancellationTokenSource _cancellationTokenSource;
+ private static readonly char[] HEX_ARRAY = "0123456789abcdef".ToCharArray();
+#endif
+
+ public AuthService()
+ {
+ _client = CortexClient.Instance;
+ }
+
+ internal AuthService(CortexClient client)
+ {
+ _client = client ?? throw new ArgumentNullException(nameof(client));
+ }
+
+ public async Task<(CortexErrorCode Code, UserDataInfo User)> InitAsync()
+ {
+ if (!await EnsureAndroidPermissionsAsync())
+ {
+ return (CortexErrorCode.PermissionsDenied, new UserDataInfo());
+ }
+
+ InitConfigFromAppConfig();
+
+#if UNITY_ANDROID || UNITY_IOS || USE_EMBEDDED_LIB
+ InitAuthentication(AppConfig.ClientId, AppConfig.ClientSecret);
+#endif
+
+ object context = GetAndroidContext();
+ _client.Init(context);
+ _client.Open();
+
+ bool isConnected = await WaitForCortexConnectionStaredAsync();
+ if (!isConnected)
+ {
+ return (CortexErrorCode.CortexConnectionError, new UserDataInfo());
+ }
+ _isInitialized = true;
+ UnityEngine.Debug.Log("AuthService: InitAsync(): WS connected.");
+
+ UserDataInfo loginData = await WaitForGetUserLoginAsync();
+ return await CompleteAuthorizationAsync(loginData);
+ }
+
+ public async Task<(CortexErrorCode Code, UserDataInfo User)> LoginAsync()
+ {
+ UnityEngine.Debug.Log("AuthService: LoginAsync(): Start login flow");
+#if UNITY_ANDROID || UNITY_IOS
+ var tcs = new TaskCompletionSource<(CortexErrorCode Code, UserDataInfo User)>();
+ UniWebViewManager.Instance.StartAuthorization(
+ onSuccess: async (authCode) => {
+ Debug.Log("UniWebView Authorization succeeded! Starting login with auth code");
+ var result = await LoginWithAuthenticationCodeAsync(authCode);
+ tcs.TrySetResult(result);
+ },
+ onError: (errorCode, errorMessage) => {
+ Debug.LogError($"Authorization failed! Error {errorCode}: {errorMessage}");
+ tcs.TrySetResult((CortexErrorCode.AuthorizationFailed, new UserDataInfo()));
+ }
+ );
+ return await tcs.Task;
+#elif USE_EMBEDDED_LIB
+ if (_authenticationSession != null)
+ {
+ _cancellationTokenSource?.Dispose();
+ _cancellationTokenSource = new CancellationTokenSource();
+ try
+ {
+ var accessTokenResponse =
+ await _authenticationSession.AuthenticateAsync(_cancellationTokenSource.Token);
+
+ return await LoginWithAuthenticationCodeAsync(accessTokenResponse.accessToken);
+ }
+ catch (AuthorizationCodeRequestException ex)
+ {
+ Debug.LogError($"{nameof(AuthorizationCodeRequestException)} " +
+ $"error: {ex.error.code}, description: {ex.error.description}, uri: {ex.error.uri}");
+ }
+ catch (AccessTokenRequestException ex)
+ {
+ Debug.LogError($"{nameof(AccessTokenRequestException)} " +
+ $"error: {ex.error.code}, description: {ex.error.description}, uri: {ex.error.uri}");
+ }
+ catch (Exception ex)
+ {
+ Debug.LogError("Exception " + ex.Message);
+ }
+ }
+ return (CortexErrorCode.UnknownError, new UserDataInfo());
+#else
+ await Task.Yield();
+ return (CortexErrorCode.UnknownError, new UserDataInfo());
+#endif
+ }
+
+ public void Logout()
+ {
+ if (string.IsNullOrEmpty(_loggedInUser.EmotivId))
+ {
+ Debug.LogWarning("Logout requested but no user is logged in.");
+ return;
+ }
+ _client.Logout(_loggedInUser.EmotivId);
+ _loggedInUser = new UserDataInfo();
+ }
+
+ private async Task<(CortexErrorCode Code, UserDataInfo User)> LoginWithAuthenticationCodeAsync(string code)
+ {
+ UnityEngine.Debug.Log("AuthService: LoginWithAuthenticationCodeAsync(): code: " + code);
+ UserDataInfo loginData = await WaitForLoginAsync(code);
+ return await CompleteAuthorizationAsync(loginData);
+ }
+
+ private async Task<(CortexErrorCode Code, UserDataInfo User)> CompleteAuthorizationAsync(UserDataInfo loginData)
+ {
+ if (string.IsNullOrEmpty(loginData.EmotivId))
+ {
+ return (CortexErrorCode.OK, loginData);
+ }
+
+ _loggedInUser = loginData;
+
+ UnityEngine.Debug.Log("AuthService: CompleteAuthorizationAsync(): User logged in: " + loginData.EmotivId);
+
+#if UNITY_ANDROID || UNITY_IOS || USE_EMBEDDED_LIB
+ bool hasAccessRight = true;
+#else
+ bool hasAccessRight = await CheckAccessRightsAsync();
+ if (!hasAccessRight)
+ {
+ _client.RequestAccess();
+ return (CortexErrorCode.NoEULAAccepted, loginData);
+ }
+#endif
+ UnityEngine.Debug.Log("AuthService: CompleteAuthorizationAsync(): Access rights granted.");
+ var authorizeResult = await AuthorizeAsync();
+ if (!authorizeResult.Success)
+ {
+ return (CortexErrorCode.AuthorizationFailed, loginData);
+ }
+
+ UnityEngine.Debug.Log("AuthService: CompleteAuthorizationAsync(): Authorized.");
+
+ License license = await GetLicenseInfoAsync();
+ UnityEngine.Debug.Log("AuthService: CompleteAuthorizationAsync(): License info retrieved." +
+ (license != null ? $" expired: {license.expired}" : " license is null"));
+ if (license == null || license.expired)
+ {
+ return (CortexErrorCode.LicenseError, loginData);
+ }
+
+ var resultUser = new UserDataInfo(loginData.LastLoginTime, authorizeResult.CortexToken, loginData.EmotivId);
+ _loggedInUser = resultUser;
+ return (CortexErrorCode.OK, resultUser);
+ }
+
+ private void InitConfigFromAppConfig()
+ {
+ string appUrl = "wss://localhost:6868";
+#if !USE_EMBEDDED_LIB && !UNITY_ANDROID && !UNITY_IOS
+ if (!string.IsNullOrEmpty(AppConfig.AppUrl))
+ {
+ appUrl = AppConfig.AppUrl;
+ }
+#endif
+
+ Config.Init(
+ AppConfig.ClientId,
+ AppConfig.ClientSecret,
+ AppConfig.AppName,
+ AppConfig.AllowSaveLogToFile,
+ appUrl,
+ "",
+ ""
+ );
+
+ MyLogger.Instance.Init(AppConfig.AppName, AppConfig.AllowSaveLogToFile);
+ }
+
+ private object GetAndroidContext()
+ {
+#if UNITY_ANDROID
+ AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
+ AndroidJavaObject currentActivity = unityPlayer.GetStatic("currentActivity");
+ return currentActivity;
+#else
+ return null;
+#endif
+ }
+
+ private Task WaitForCortexConnectionStaredAsync()
+ {
+ var tcs = new TaskCompletionSource();
+ EventHandler handler = null;
+ handler = (sender, isConnected) =>
+ {
+ _client.CortexConnectionStared -= handler;
+ tcs.TrySetResult(isConnected);
+ };
+ _client.CortexConnectionStared += handler;
+ return tcs.Task;
+ }
+
+ private Task WaitForGetUserLoginAsync()
+ {
+ var tcs = new TaskCompletionSource();
+ EventHandler handler = null;
+ handler = (sender, data) =>
+ {
+ _client.GetUserLoginDone -= handler;
+ UnityEngine.Debug.Log("AuthService: WaitForGetUserLoginAsync(): User logged in: " + data.EmotivId);
+ if (!string.IsNullOrEmpty(data.EmotivId))
+ {
+ _loggedInUser = data;
+ }
+ tcs.TrySetResult(data);
+ };
+ _client.GetUserLoginDone += handler;
+ _client.GetUserLogin();
+ return tcs.Task;
+ }
+
+ private Task WaitForLoginAsync(string authCode)
+ {
+ var tcs = new TaskCompletionSource();
+ EventHandler handler = null;
+ handler = (sender, data) =>
+ {
+ _client.LoginDone -= handler;
+ UnityEngine.Debug.Log("AuthService: WaitForLoginAsync(): User logged in: " + data.EmotivId);
+ if (!string.IsNullOrEmpty(data.EmotivId))
+ {
+ _loggedInUser = data;
+ }
+ tcs.TrySetResult(data);
+ };
+ _client.LoginDone += handler;
+ _client.LoginWithAuthenticationCode(authCode);
+ return tcs.Task;
+ }
+
+ private Task CheckAccessRightsAsync()
+ {
+ var tcs = new TaskCompletionSource();
+
+ EventHandler okHandler = null;
+ EventHandler errorHandler = null;
+
+ okHandler = (sender, hasAccessRight) =>
+ {
+ _client.HasAccessRightOK -= okHandler;
+ _client.ErrorMsgReceived -= errorHandler;
+ tcs.TrySetResult(hasAccessRight);
+ };
+
+ errorHandler = (sender, error) =>
+ {
+ if (error.MethodName != "hasAccessRight")
+ {
+ return;
+ }
+ _client.HasAccessRightOK -= okHandler;
+ _client.ErrorMsgReceived -= errorHandler;
+ tcs.TrySetResult(false);
+ };
+
+ _client.HasAccessRightOK += okHandler;
+ _client.ErrorMsgReceived += errorHandler;
+ _client.HasAccessRights();
+ return tcs.Task;
+ }
+
+ private Task<(bool Success, string CortexToken)> AuthorizeAsync()
+ {
+ var tcs = new TaskCompletionSource<(bool Success, string CortexToken)>();
+
+ EventHandler okHandler = null;
+ EventHandler errorHandler = null;
+ EventHandler eulaHandler = null;
+
+ okHandler = (sender, token) =>
+ {
+ Cleanup();
+ tcs.TrySetResult((true, token));
+ };
+
+ eulaHandler = (sender, token) =>
+ {
+ Cleanup();
+ tcs.TrySetResult((false, token));
+ };
+
+ errorHandler = (sender, error) =>
+ {
+ if (error.MethodName != "authorize")
+ {
+ return;
+ }
+ Cleanup();
+ tcs.TrySetResult((false, ""));
+ };
+
+ void Cleanup()
+ {
+ _client.AuthorizeOK -= okHandler;
+ _client.EULANotAccepted -= eulaHandler;
+ _client.ErrorMsgReceived -= errorHandler;
+ }
+
+ _client.AuthorizeOK += okHandler;
+ _client.EULANotAccepted += eulaHandler;
+ _client.ErrorMsgReceived += errorHandler;
+
+ _client.Authorize(string.Empty, 5000);
+ return tcs.Task;
+ }
+
+ private Task GetLicenseInfoAsync()
+ {
+ var tcs = new TaskCompletionSource();
+ EventHandler<(CortexErrorCode error, License data)> handler = null;
+
+ handler = (sender, result) =>
+ {
+ _client.GetLicenseInfoResult -= handler;
+ UnityEngine.Debug.Log("AuthService: GetLicenseInfoAsync(): result code: " + result.error);
+ if (result.error == CortexErrorCode.OK)
+ {
+ tcs.TrySetResult(result.data);
+ }
+ else
+ {
+ tcs.TrySetResult(null);
+ }
+ };
+
+ _client.GetLicenseInfoResult += handler;
+
+ _client.GetLicenseInfo();
+ return tcs.Task;
+ }
+
+ private async Task EnsureAndroidPermissionsAsync()
+ {
+#if UNITY_ANDROID
+ if (HasAllPermissions())
+ {
+ return true;
+ }
+
+ foreach (var permission in GetRequiredPermissions())
+ {
+ if (!Permission.HasUserAuthorizedPermission(permission))
+ {
+ Permission.RequestUserPermission(permission);
+ while (!Permission.HasUserAuthorizedPermission(permission))
+ {
+ await Task.Delay(100);
+ }
+ }
+ }
+
+ return HasAllPermissions();
+#else
+ await Task.Yield();
+ return true;
+#endif
+ }
+
+#if UNITY_ANDROID
+ private static string[] GetRequiredPermissions()
+ {
+ if (GetAndroidVersion() >= 31)
+ {
+ return new[] {
+ "android.permission.ACCESS_FINE_LOCATION",
+ "android.permission.BLUETOOTH_SCAN",
+ "android.permission.BLUETOOTH_CONNECT"
+ };
+ }
+
+ return new[] {
+ "android.permission.ACCESS_FINE_LOCATION",
+ "android.permission.BLUETOOTH"
+ };
+ }
+
+ private static bool HasAllPermissions()
+ {
+ foreach (var permission in GetRequiredPermissions())
+ {
+ if (!Permission.HasUserAuthorizedPermission(permission))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static int GetAndroidVersion()
+ {
+ string osInfo = SystemInfo.operatingSystem;
+ if (osInfo.Contains("Android"))
+ {
+ int apiIndex = osInfo.IndexOf("API-");
+ if (apiIndex != -1)
+ {
+ string apiLevel = osInfo.Substring(apiIndex + 4).Split(' ')[0];
+ if (int.TryParse(apiLevel, out int androidVersion))
+ {
+ return androidVersion;
+ }
+ }
+ }
+ return 0;
+ }
+#endif
+
+ private void InitAuthentication(string clientId, string clientSecret)
+ {
+#if USE_EMBEDDED_LIB || UNITY_ANDROID || UNITY_IOS
+ string server = "";
+#if DEV_SERVER
+ Debug.Log("Development build detected. Using development server.");
+ server = "cerebrum-dev.emotivcloud.com";
+#else
+ Debug.Log("Production build detected. Using production server.");
+ server = "cerebrum.emotivcloud.com";
+#endif
+ string hash = Md5(clientId);
+ string prefixRedirectUrl = "emotiv-" + hash;
+ string redirectUrl = prefixRedirectUrl + "://authorize";
+ string serverUrl = $"https://{server}";
+#if UNITY_ANDROID || UNITY_IOS
+ string authorizationUrl = $"https://{server}/api/oauth/authorize/?response_type=code" +
+ $"&client_id={Uri.EscapeDataString(clientId)}" +
+ $"&redirect_uri={redirectUrl}" + "&hide_signup=1&hide_social_signin=1";
+ UniWebViewManager.Instance.Init(
+ authorizationUrl,
+ prefixRedirectUrl
+ );
+#else
+ _crossPlatformBrowser = new CrossPlatformBrowser();
+ _crossPlatformBrowser.platformBrowsers.Add(RuntimePlatform.WindowsEditor, new WindowsSystemBrowser());
+ _crossPlatformBrowser.platformBrowsers.Add(RuntimePlatform.WindowsPlayer, new WindowsSystemBrowser());
+ _crossPlatformBrowser.platformBrowsers.Add(RuntimePlatform.OSXEditor, new DeepLinkBrowser());
+ _crossPlatformBrowser.platformBrowsers.Add(RuntimePlatform.OSXPlayer, new DeepLinkBrowser());
+
+#if UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN
+ // Deep linking is not supported on Windows (except UWP), so RegistryConfig is used to handle this case.
+ new RegistryConfig(prefixRedirectUrl).Configure();
+#endif
+
+ var configuration = new AuthorizationCodeFlow.Configuration()
+ {
+ clientId = clientId,
+ clientSecret = clientSecret,
+ redirectUri = redirectUrl,
+ scope = ""
+ };
+ var auth = new MockServerAuth(configuration, serverUrl);
+ _authenticationSession = new AuthenticationSession(auth, _crossPlatformBrowser);
+ _authenticationSession.loginTimeout = TimeSpan.FromSeconds(600);
+#endif
+#endif
+ }
+
+#if USE_EMBEDDED_LIB || UNITY_ANDROID || UNITY_IOS
+ private string BytesToHex(byte[] bytes)
+ {
+ char[] hexChars = new char[bytes.Length * 2];
+ for (int j = 0; j < bytes.Length; ++j)
+ {
+ int v = bytes[j] & 0xFF;
+ hexChars[j * 2] = HEX_ARRAY[v >> 4];
+ hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
+ }
+ return new string(hexChars);
+ }
+
+ private string Md5(string s)
+ {
+ try
+ {
+ using (var md5 = MD5.Create())
+ {
+ byte[] inputBytes = Encoding.UTF8.GetBytes(s);
+ byte[] hashBytes = md5.ComputeHash(inputBytes);
+ return BytesToHex(hashBytes);
+ }
+ }
+ catch (Exception e)
+ {
+ Debug.LogError(e);
+ return string.Empty;
+ }
+ }
+#endif
+ }
+}
diff --git a/Src/Runtime/SDK/Auth/IAuthService.cs b/Src/Runtime/SDK/Auth/IAuthService.cs
new file mode 100644
index 0000000..958f9f2
--- /dev/null
+++ b/Src/Runtime/SDK/Auth/IAuthService.cs
@@ -0,0 +1,31 @@
+using System.Threading.Tasks;
+using Emotiv.Cortex.Models;
+using EmotivUnityPlugin; // TODO (Tung Nguyen): remove this line when move all old code to new SDK
+
+namespace Emotiv.Cortex.Service
+{
+ public interface IAuthService
+ {
+
+ ///
+ /// Initializes the SDK and attempts authorization using an already logged-in user.
+ ///
+ ///
+ /// A tuple containing the and the resulting .
+ ///
+ Task<(CortexErrorCode Code, UserDataInfo User)> InitAsync();
+
+ ///
+ /// Starts the interactive login flow and attempts authorization.
+ ///
+ ///
+ /// A tuple containing the and the resulting .
+ ///
+ Task<(CortexErrorCode Code, UserDataInfo User)> LoginAsync();
+
+ ///
+ /// Logs out the current user and clears local auth state.
+ ///
+ void Logout();
+ }
+}
\ No newline at end of file
diff --git a/Src/Runtime/SDK/ServiceFactory.cs b/Src/Runtime/SDK/ServiceFactory.cs
new file mode 100644
index 0000000..adf6bfb
--- /dev/null
+++ b/Src/Runtime/SDK/ServiceFactory.cs
@@ -0,0 +1,10 @@
+namespace Emotiv.Cortex.Service
+{
+ public static class ServiceFactory
+ {
+ public static IAuthService GetAuthService()
+ {
+ return AuthService.Instance;
+ }
+ }
+}
diff --git a/Src/Types.cs b/Src/Types.cs
index 314c356..0f900ed 100644
--- a/Src/Types.cs
+++ b/Src/Types.cs
@@ -381,6 +381,10 @@ public UserDataInfo(double time = 0, string token = "", string emotivId = "") {
public double LastLoginTime { get; set; }
public string CortexToken { get; set;}
public string EmotivId { get; set;}
+ public bool IsAuthorized()
+ {
+ return !string.IsNullOrEmpty(CortexToken) && !string.IsNullOrEmpty(EmotivId);
+ }
public UserDataInfo(SerializationInfo info, StreamingContext ctxt) {
LastLoginTime = (double)info.GetValue("lastLoginTime", typeof(double));
diff --git a/Src/WebsocketCortexClient.cs b/Src/WebsocketCortexClient.cs
index 9d4145f..778064e 100644
--- a/Src/WebsocketCortexClient.cs
+++ b/Src/WebsocketCortexClient.cs
@@ -152,7 +152,7 @@ private void WebSocketClient_MessageReceived(object sender, MessageReceivedEvent
///
private void WebSocketClient_Closed(object sender, EventArgs e)
{
- OnWSConnected(false);
+ OnCortexConnectionStared(false);
// start connecting cortex service again
if (_wscTimer != null)
_wscTimer.Start();
@@ -165,7 +165,7 @@ private void WebSocketClient_Opened(object sender, EventArgs e)
{
m_OpenedEvent.Set();
if (_wSC.State == WebSocketState.Open) {
- OnWSConnected(true);
+ OnCortexConnectionStared(true);
// stop timer
_wscTimer.Stop();
@@ -183,7 +183,7 @@ private void WebSocketClient_Error(object sender, SuperSocket.ClientEngine.Error
if (e.Exception.InnerException != null) {
UnityEngine.Debug.Log(e.Exception.InnerException.GetType());
- OnWSConnected(false);
+ OnCortexConnectionStared(false);
// start connecting cortex service again
_wscTimer.Start();
}