From 576ee9b8b9be135d131d38b3c31a73697372fc7b Mon Sep 17 00:00:00 2001 From: Patrick Kitchens Date: Thu, 26 Apr 2018 00:58:00 -0500 Subject: [PATCH] OAuth implemented. Data is ? --- MoCha/Assets/Scenes/Main.unity | Bin 51304 -> 53688 bytes MoCha/Assets/Scripts/CustomAndroidPlugin.cs | 23 + .../Scripts/CustomAndroidPlugin.cs.meta | 13 + MoCha/Assets/Scripts/FitBitAPI.cs | 563 +++++++++ MoCha/Assets/Scripts/FitBitAPI.cs.meta | 13 + MoCha/Assets/Scripts/FitbitCaller.cs | 15 + MoCha/Assets/Scripts/FitbitCaller.cs.meta | 13 + MoCha/Assets/Scripts/FitbitData.cs | 54 + MoCha/Assets/Scripts/FitbitData.cs.meta | 13 + MoCha/Assets/Scripts/JSONObject.cs | 1017 +++++++++++++++++ MoCha/Assets/Scripts/JSONObject.cs.meta | 13 + MoCha/Assets/Scripts/OAuth2AccessToken.cs | 10 + .../Assets/Scripts/OAuth2AccessToken.cs.meta | 13 + MoCha/ProjectSettings/ProjectSettings.asset | Bin 55939 -> 56395 bytes 14 files changed, 1760 insertions(+) create mode 100644 MoCha/Assets/Scripts/CustomAndroidPlugin.cs create mode 100644 MoCha/Assets/Scripts/CustomAndroidPlugin.cs.meta create mode 100644 MoCha/Assets/Scripts/FitBitAPI.cs create mode 100644 MoCha/Assets/Scripts/FitBitAPI.cs.meta create mode 100644 MoCha/Assets/Scripts/FitbitCaller.cs create mode 100644 MoCha/Assets/Scripts/FitbitCaller.cs.meta create mode 100644 MoCha/Assets/Scripts/FitbitData.cs create mode 100644 MoCha/Assets/Scripts/FitbitData.cs.meta create mode 100644 MoCha/Assets/Scripts/JSONObject.cs create mode 100644 MoCha/Assets/Scripts/JSONObject.cs.meta create mode 100644 MoCha/Assets/Scripts/OAuth2AccessToken.cs create mode 100644 MoCha/Assets/Scripts/OAuth2AccessToken.cs.meta diff --git a/MoCha/Assets/Scenes/Main.unity b/MoCha/Assets/Scenes/Main.unity index 4653ae1b6e8fb26c5583435aa7083105930eb756..1cef1c2cb4c2a6dc7a4a29afd11485808ca1f395 100644 GIT binary patch delta 5469 zcmc&&dsI}%8K28@1;iD>C&KcQm+~~CCz^n6fJQ7dK8k4!P$h^#USg1>x{IQaYE&3N zlh%k4P!ko*O)3hCC|29XRNK>NQ)BcTO-PSc6SZoi(%*OQT-lAAmh_+A!@PdqH#6UB zzL}+GqkY?U`yj@ee$3dJHpbXs^b66ln5gKuh}ekem!jFj-rioxe_v3#B$@dNX_#b| z>!oam5oRVaI~HNj%r`JWhZ2Y)F*EmcDz!lk6VyhhNE_5~f_l|ygAM9?H+xOG|PK^7+Gn-$HJi z&W6Ef$i+cT_Wyxl2WW;cvO>A3OTHI2=6gd2t64GgVsi+Ok9F2}Ro*YWb-86q$+2j= zF7xy@zAOBd{_dzO5*kql{>})J;%tO^3Y?VlH3jCNh;&oTV&)Ck2w)LC*yv{jf#0;WanRWk34p%A?uLyJ1cJ|#Kv#Mnyozt5(RtRR=0LL@$P zf@L%#r#yCsPED{pLmG$08Irx?eC#a3R%0;;2~#E|*`SJ~Bk!Ixjy&8_OA64LZK&1p z8?{arQ|P>q$`*U_sJgLo<&=?X0&q~wt+5Rg`yPDCGM#DE(SrKy)0Rfps0Kl;u}P&9 zgM%l3`m~Q_9t{Uf@@bi8u#sGnU?hzwYrJZlgWee$%U>BUQ;dF`sHxkTsN;i@EY+ox z^j&pRljc|$MB^B!%EJ#Q{TXZiSC8Y69G6R?_{-?`F zzWw4u2T8|o&Hq1G)V&(&zRB>PO`!=cKpvcDJO7$pMFP$KkzFcB! zW22ysU22P(q*CRQrJvgh))&O&yizsNt+*gX7+1uH z{k>6-%igPSzWbSmtfF8-)edO@zfjdLnUuUReeLfhS3Y!Izh}s+)>l~GQQF%`OWAb7 zZ9c8|I$t9{RXr7o>1*ihhtzZ;jrY`~NkR6X+`aojL3$I9{j&FF#V zGjuK`X|3lx~6D1*R$2lyMsA!Gukq*6U(&vjQ@97Z_QFT(Co68=-0%#$^7T0=o!| zei;ox*(I>yP*vOHJ%LpLGr~rm;QQ!>TacL1R`B-3?P&EW& z3d%nUtP0p>jOm5sf8k*$8fJj1WoXF?0Rn3PHXiGd6$1r!4q9pJnSumJYH15j)$N)H zezqByX4Yu}(?eCV!UzT?g*?En5OZBPaF$CVwr)A z6``d~;skaMm}csU0xLwkkCjyGOcGcF@-{4OQlh$g;E#%F!IJ=2iv?>0mKNN16uH29 zfRQFpoYe|!B39FmVx7PWfst}J_H3oDP}$=mFwLE-bn{g(9tWrcOytj+nX8weQGN$& z(a#08EUZtvT7l`q`+~JjV1}`MapcZl|8?rTi)Ag!qK>HdwmttR{7a{o`d@rI1#2=! z{j}QZ8Cw68bMJFnKkjAMSE!E+$+UHxE&5)3^p+_7yI0b`IIwJ4;-M4O-)s*vl=7r4 zF?!vazc?KZJU^}Bc+L!|yizYO*^*!<>EsW$y4XA7{$PwBc_)P5c-w*ZymOc9x4Ln| z5nYIusX>2~$*GSt#u9T&ex6%0Eh{fCyO?i0Y;>UX&lfcN^VXgI_89O>4MTWDmp?ab z*WoqLwLJj!gSKGq)~H8sbYmRq#Ro$8^2R~vy`hXhI}pHo8a;_q8Nbo!%V#wWC(fow z)LHGpyt|3$13M!9ke->r)qq<4$iWRccgTp!H-LvWCrJ)+QS)ndcwSuF`8OvW2%iRX zX=@lS+@+Uw?@qLn9C^a-A(9(k{+sQ1O8MQ0`yF^n zOB%2(Ey1Xdx3rL%BDDladuj=i`pA83+R`N@1&b5%7ZexdE=bE;nvh6{82p;pwliEVX`LlOir?+~RUwZ$#0zuWE!436WC$1q!-(eW!gofh#tx_bz>?|cGsPwK2BxnXk2Q!YCiE=gYS-73EE ln2twvdk&y?fXmr{$vR%%9ZsmElgqn5|BkMTBhyak{tX#2!Ug~U delta 4778 zcmc&&Yfx3!72fA^@8t@LSHOT#;36Q3ycN+#D;NUkgoq)@po0T86#efhQb~A_V!{O9$XaN$wGQ+yLAd$-QmgJpgyI~FG(?9|)1qkV*y>NKN2?~u3E$Bb2XBhUz(wL!qk~_dF+m#! zwPJ0=#$Bbq_Pg3RKIsR4^lh#>PZME2 z`=mGOj*wsr-LJG$eP~od-v%4Y#$q|F;8Da4me^lo_4(@z#S$aJ4 zQgr+c;DvG(r-py5jd8ccqtwe|pHe6>{L=UBA2OGv)yiMKx;T zxbLk8=<6^31@*@7(emb@Rrn0J$$7JB?g~1bup4UQ5;<=fiC=1x_I{E9QgZGj{Vdq} zaf2lH=A<~f_N*5LPWC|=Mo!M)GxmHEtKF7lF7-GpUqcyp?`+ZbFU!@je$3=cPO!^;J;%;`0_ zT*#1HqaxbbcnUWR6vT`;)Aq_T#LKbX4<_d#B4?k*j(vHx^q zHk;$F5HRN%%vR0`@Dd_x@v3F%KQ1gG_vnd~y~{?;bM_Bv*B$DkS+NRAR}o&+Tj~eR zuRhVN?UA>Z(GclTtaSKCbI(~cwpTy)<2suXpB!lM({iy_POn9=(?6dcTxmLJMeaB) zEWIVy=FZiV=Go5&?itu1@?W6);ONIqls?>EptpC=$0TqjZvt1{jeKK2JMtYAQeZa~ zDhF@gQ{IOBg3nD7|I=qdZCZ5Ss(4c4lK*2qYM=W+Pp00@jQviR+1at;0}o{gWt9xp z#rI!)M$1oswPSR(6jRC{mxZOA^VOyLaqBNBzbDkBNp`cOYFo3-f(1L*@v@`q23nRT z4Ro(#13~L~Si7eaC|C?mVtI=pCS}}-qYD(pHy$$Fe0pk*2^sR5* z9cV{L8tvLQ3+cY1-6y`-hYe6qR%I&@VLnvQ)co%9pC9_<@DsmUmHH4ezZZ}Bx{|3~ zS4w9_LBSxt4iuIWO+g3DmmH&+%l=CR2hyi&TdfGRF!yqO+xf2B?^?13B**5|nOJdo; z5SQ@5)AD*+Cgs4WS zYcaCn_B{egWfv3P_!9yF(^Lt?qWsz7sTjlFSy0tV3o%+MDmu_A;&8T$D4 z5H7J3WeCeq7$Lze=;SRPIBOB?KSP!tRpdZHb7m2M2E<>dQ;1d5*f}O#AMe~(Udm&gN1s|?W z7+K*RiM0V^@A;$iH;Dzh^tF5k{JX@q1Iqyowc@VCx`1f`!@ijE8{6|3M0uzX#TeO0 zvcw!X7geA+GgBp24J=v~(lm*6;{Ir)x?w8UN?(n|TM8Md-2*erS zv?3r;DFv4SGmg_9iB$tDf(%Zi*z73Mj5yJpjs4sru_9oNkYT4^%Xw)P1`!|G*B3}) zCn81SzoV;^U<$%83TcnTvT>-4=!VkeH|IX~_W1>$;J*V8IJQ51Gb4&k@I1S+BFmV! zdp>y5m?uBy{UXn8c)He}=BeyKHTM$f1XA!}VA=%Bhd zyc=30sicni3oploI$+=B?P52yAG)TfUV4|^(|(@x+qw+8_trz|l(&~ya78*)f6ZnG z>ngp`9!W(kKOs^=S~S`;hws(y67&WElx@5ObZ_(BHtoH?Qlr9D3@!+Ye}iI7P> zdrlcfU!TnZl65W;@73o{t9kD{rGVM+?l+1;b?+^upEMp(tZHb}KB-snq4wq)Ww=^= zAzx9Pbmd|O{`^aQYb3?C1cT0MNyeK~;zUaL&FOF=9W(LfR4@`kt(RPw1=WSn9VF37 z8(POuLTiAWEta!Mpc`5r!MnRP+2%yNZo1eKLYbHSfgy1txaFG@=Tr|}K45VedrzC% z{V1m`2{F9VhU=Ujx3Bt(4<{;$AF{ZGx;}D{yWQXB^=a62J6&l{R_yAP_M6{|SIc5` Gr{iDaH|x^? diff --git a/MoCha/Assets/Scripts/CustomAndroidPlugin.cs b/MoCha/Assets/Scripts/CustomAndroidPlugin.cs new file mode 100644 index 0000000..a040bd8 --- /dev/null +++ b/MoCha/Assets/Scripts/CustomAndroidPlugin.cs @@ -0,0 +1,23 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using Assets.Scripts.Fitbit; + + +namespace Assets.Scripts +{ + public class CustomAndroidPlugin : MonoBehaviour + { + + public GameObject Manager; + + public void PassReturnCode(string value) + { + Manager.GetComponent().SetReturnCodeFromAndroid(value); + + } + + } + + +} diff --git a/MoCha/Assets/Scripts/CustomAndroidPlugin.cs.meta b/MoCha/Assets/Scripts/CustomAndroidPlugin.cs.meta new file mode 100644 index 0000000..a23005d --- /dev/null +++ b/MoCha/Assets/Scripts/CustomAndroidPlugin.cs.meta @@ -0,0 +1,13 @@ +fileFormatVersion: 2 +guid: 80685d60eb42cd340a5c76d410187801 +timeCreated: 1524716619 +licenseType: Free +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MoCha/Assets/Scripts/FitBitAPI.cs b/MoCha/Assets/Scripts/FitBitAPI.cs new file mode 100644 index 0000000..4dfdbca --- /dev/null +++ b/MoCha/Assets/Scripts/FitBitAPI.cs @@ -0,0 +1,563 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Xml; +using System.Xml.Linq; +using Newtonsoft.Json; +using UnityEngine; + +namespace Assets.Scripts.Fitbit +{ + + public class FitBitAPI : MonoBehaviour + { + + private const string _consumerSecret = "69307b9f332caf9946ef4e23cabde2e4"; + private const string _clientId = "22CX4L"; + private const string _callbackURL = "http://localhost/callback"; + //If you're making an app for Android, fill in your custom scheme here from Fitbit + //if you don't know how to do the callback through a native browser on a mobile device + //http://technicalartistry.blogspot.ca/2016/01/fitbit-unity-oauth-2-and-native.html + //can probably help :) + private const string CustomAndroidScheme = "mochaapp://TheCallback"; + + private const string _tokenUrl = "https://api.fitbit.com/oauth2/token"; + private const string _baseGetUrl = "https://api.fitbit.com/1/user/-/"; + private const string _profileUrl = _baseGetUrl + "profile.json/"; + private const string _activityUrl = _baseGetUrl + "activities/"; + + private string _distanceUrl = _activityUrl + "distance/date/" + _currentDateTime + "/1d.json"; + + private string _stepsUrl = _activityUrl + "steps/date/" + _currentDateTime + "/1d.json"; + + private string _caloriesUrl = _activityUrl + "calories/date/" + _currentDateTime + "/1d.json"; + + private string _sleepUrl = _baseGetUrl + "sleep/minutesAsleep/date/" + _currentDateTime + "/" + _currentDateTime + ".json"; + + private static string _currentDateTime = GetCurrentDate(); + + private string _returnCode; + private WWW _wwwRequest; + private bool _bGotTheData = false; + private bool _bFirstFire = true; + + private OAuth2AccessToken _oAuth2 = new OAuth2AccessToken(); + public FitbitData _fitbitData = new FitbitData(); + + //Debug String for Android + private string _statusMessage; + + private string CallBackUrl + { + get + { + //determine which platform we're running on and use the appropriate url + if (Application.platform == RuntimePlatform.WindowsEditor) + return WWW.EscapeURL(_callbackURL); + else if (Application.platform == RuntimePlatform.Android) + { + return WWW.EscapeURL(CustomAndroidScheme); + } + else + { + return WWW.EscapeURL(CustomAndroidScheme); + } + } + } + + public void Start() + { + DontDestroyOnLoad(this); + } + + private void OnGUI() + { + if (!_bGotTheData && !string.IsNullOrEmpty(_statusMessage) && _bFirstFire) + { + _bFirstFire = false; + } + GUI.Label(new Rect(10, 10, 500, 500), "Number Steps: " + _fitbitData.CurrentSteps); + + GUI.Label(new Rect(10, 20, 500, 500), "Calories: " + _fitbitData.CurrentCalories); + GUI.Label(new Rect(10, 40, 500, 500), "Distance Walked: " + _fitbitData.CurrentDistance); + GUI.Label(new Rect(10, 60, 500, 500), "Return Code " + _returnCode); + } + + public void LoginToFitbit() + { + //we'll check to see if we have the RefreshToken in PlayerPrefs or not. + //if we do, then we'll use the RefreshToken to get the data + //if not then we will just do the regular ask user to login to get data + //then save the tokens correctly. + + if (PlayerPrefs.HasKey("FitbitRefreshToken")) + { + UseRefreshToken(); + } + else + { + UserAcceptOrDeny(); + } + + } + public void UserAcceptOrDeny() + { + //we don't have a refresh token so we gotta go through the whole auth process. + var url = + "https://www.fitbit.com/oauth2/authorize?response_type=code&client_id=" + _clientId + "&redirect_uri=" + + CallBackUrl + + "&scope=activity%20nutrition%20heartrate%20location%20profile%20sleep%20weight%20social"; + Application.OpenURL(url); + // print(url); +#if UNITY_EDITOR +#endif + } + + public void ClearRefreshCode() + { + PlayerPrefs.DeleteKey("FitbitRefreshToken"); + Debug.Log("Refresh Token has been CLEARED!"); + } + + private void UseReturnCode() + { + Debug.Log("return code isn't empty"); + //not empty means we put a code in + var plainTextBytes = System.Text.Encoding.UTF8.GetBytes(_clientId + ":" + _consumerSecret); + var encoded = Convert.ToBase64String(plainTextBytes); + + var form = new WWWForm(); + form.AddField("client_id", _clientId); + form.AddField("grant_type", "authorization_code"); + form.AddField("redirect_uri", WWW.UnEscapeURL(CallBackUrl)); + form.AddField("code", _returnCode); + + var headers = form.headers; + headers["Authorization"] = "Basic " + encoded; + + _wwwRequest = new WWW(_tokenUrl, form.data, headers); + StartCoroutine(WaitForAccess(_wwwRequest)); + + //DIRTY DIRTY HACK + while (!_wwwRequest.isDone) + { + } + + Debug.Log("Token: " + _wwwRequest.text); + Debug.Log("parsing token"); + + var parsed = new JSONObject(_wwwRequest.text); + ParseAccessToken(parsed); + Debug.Log("\nParsed Token: " + _oAuth2.Token); + + //now that we have the Auth Token, Lets use it and get data. + GetAllData(); + Debug.Log("Steps from Fitbit: " + _fitbitData.CurrentSteps); + _bGotTheData = true; + } + + public void UseRefreshToken() + { + Debug.Log("Using Refresh Token"); + var plainTextBytes = Encoding.UTF8.GetBytes(_clientId + ":" + _consumerSecret); + var encoded = Convert.ToBase64String(plainTextBytes); + + var form = new WWWForm(); + form.AddField("grant_type", "refresh_token"); + form.AddField("refresh_token", PlayerPrefs.GetString("FitbitRefreshToken")); + + var headers = form.headers; + headers["Authorization"] = "Basic " + encoded; + + _wwwRequest = new WWW(_tokenUrl, form.data, headers); + StartCoroutine(WaitForAccess(_wwwRequest)); + //DIRTY DIRTY HACK + while (!_wwwRequest.isDone) + { + } + + Debug.Log("RefreshToken wwwText: " + _wwwRequest.text); + //check to see if it's errored or not + //we have an error and thus should just redo the Auth. + if (!String.IsNullOrEmpty(_wwwRequest.error)) + { + PlayerPrefs.DeleteKey("FitbitRefreshToken"); + UserAcceptOrDeny(); + UseReturnCode(); + GetAllData(); + } + else + { + Debug.Log("Using the Auth Token (UseRefreshToken)"); + //no errors so parse the accessToken and update everything :) + var parsed = new JSONObject(_wwwRequest.text); + ParseAccessToken(parsed); + GetAllData(); + } + } + + public void SetReturnCodeFromAndroid(string code) + { + if (string.IsNullOrEmpty(code)) + return; + //we passed the full URL so we'll have to extract the + //We will add 6 to the string lenght to account for "?code=" + _returnCode = code.Substring(CustomAndroidScheme.Length + 6); + Debug.Log("Return Code is: " + _returnCode); + + UseReturnCode(); + } + + public void SetReturnCode(string code) + { + if (string.IsNullOrEmpty(code)) + return; + + _returnCode = code; + UseReturnCode(); + } + + + + + public void GetAllData() + { + GetProfileData(); + GetAllRelevantData(); + BuildProfile(); + + //make sure the loading screen is open and change message + _fitbitData.LastSyncTime = DateTime.Now.ToUniversalTime(); + Debug.Log("LastSyncTime: " + DateTime.Now.ToUniversalTime().ToString("g")); + } + + private void GetAllRelevantData() + { + GetSteps(); + GetDistance(); + GetCalories(); + GetSleep(); + } + + #region GetData + private void GetProfileData() + { + + //time for Getting Dataz + var headers = new Dictionary(); + headers["Authorization"] = "Bearer " + _oAuth2.Token; + + _wwwRequest = new WWW(_profileUrl, null, headers); + Debug.Log("Doing GET Request"); + StartCoroutine(WaitForAccess(_wwwRequest)); + + //DIRTY DIRTY HACK + while (!_wwwRequest.isDone) + { + } + + ParseProfileData(_wwwRequest.text); + + } + + private void GetCalories() + { + //time for Getting Dataz + var headers = new Dictionary(); + headers["Authorization"] = "Bearer " + _oAuth2.Token; + + Debug.Log("Calories URL is: " + _caloriesUrl); + _wwwRequest = new WWW(_caloriesUrl, null, headers); + Debug.Log("Doing Calories GET Request"); + StartCoroutine(WaitForAccess(_wwwRequest)); + + //DIRTY DIRTY HACK + while (!_wwwRequest.isDone) + { + } + ParseCaloriesData(_wwwRequest.text); + } + + private void GetDistance() + { + //time for Getting Dataz + var headers = new Dictionary(); + headers["Authorization"] = "Bearer " + _oAuth2.Token; + + Debug.Log("Distance URL is: " + _distanceUrl); + _wwwRequest = new WWW(_distanceUrl, null, headers); + Debug.Log("Doing Distance GET Request"); + StartCoroutine(WaitForAccess(_wwwRequest)); + + //DIRTY DIRTY HACK + while (!_wwwRequest.isDone) + { + } + + ParseDistanceData(_wwwRequest.text); + } + + private void GetSteps() + { + //time for Getting Dataz + var headers = new Dictionary(); + headers["Authorization"] = "Bearer " + _oAuth2.Token; + + Debug.Log("Steps URL is: " + _stepsUrl); + _wwwRequest = new WWW(_stepsUrl, null, headers); + Debug.Log("Doing Steps GET Request"); + StartCoroutine(WaitForAccess(_wwwRequest)); + + //DIRTY DIRTY HACK + while (!_wwwRequest.isDone) + { + } + + ParseStepsData(_wwwRequest.text); + } + + private void GetSleep() + { + //time for Getting Dataz + var headers = new Dictionary(); + headers["Authorization"] = "Bearer " + _oAuth2.Token; + + Debug.Log("Sleep URL is: " + _sleepUrl); + _wwwRequest = new WWW(_sleepUrl, null, headers); + Debug.Log("Doing Sleep GET Request"); + StartCoroutine(WaitForAccess(_wwwRequest)); + + //DIRTY DIRTY HACK + while (!_wwwRequest.isDone) + { + } + + ParseSleepData(_wwwRequest.text); + } + + private void BuildProfile() + { + var imageWWW = new WWW(_fitbitData.ProfileData["avatar"]); + //DIRTY DIRTY HACK + while (!imageWWW.isDone) + { + } + + Debug.Log(_fitbitData.RawProfileData["fullName"]); + + //we should check to see if there is "data" already + if (_fitbitData.ProfileData.Count != 0) + { + foreach (KeyValuePair kvp in _fitbitData.ProfileData) + { + if (kvp.Key == "avatar") + continue; + + //put a space between the camelCase + var tempKey = Regex.Replace(kvp.Key, "(\\B[A-Z])", " $1"); + //then capitalize the first letter + UppercaseFirst(tempKey); + } + } + + _bGotTheData = true; + } + #endregion + + #region Parsing + private void ParseAccessToken(JSONObject parsed) + { + var dict = parsed.ToDictionary(); + foreach (KeyValuePair kvp in dict) + { + if (kvp.Key == "access_token") + { + _oAuth2.Token = kvp.Value; + PlayerPrefs.SetString("FitbitAccessToken", kvp.Value); + } + else if (kvp.Key == "expires_in") + { + var num = 0; + Int32.TryParse(kvp.Value, out num); + _oAuth2.ExpiresIn = num; + + } + else if (kvp.Key == "refresh_token") + { + _oAuth2.RefreshToken = kvp.Value; + Debug.Log("REFRESH TOKEN: " + kvp.Value); + PlayerPrefs.SetString("FitbitRefreshToken", kvp.Value); + Debug.Log("Token We Just Store: " + PlayerPrefs.GetString("FitbitRefreshToken")); + } + else if (kvp.Key == "token_type") + { + _oAuth2.TokenType = kvp.Value; + PlayerPrefs.SetString("FitbitTokenType", kvp.Value); + } + } + } + + private void ParseProfileData(string data) + { + Debug.Log("inserting json data into fitbitData.RawProfileData"); + //Debug.LogWarning(data); + XmlDocument xmldoc = JsonConvert.DeserializeXmlNode(data); + + var doc = XDocument.Parse(xmldoc.InnerXml); + + + doc.Descendants("topBadges").Remove(); + foreach (XElement xElement in doc.Descendants()) + { + //Debug.Log(xElement.Name.LocalName + ": Value:" + xElement.Value);V + if (!_fitbitData.RawProfileData.ContainsKey(xElement.Name.LocalName)) + _fitbitData.RawProfileData.Add(xElement.Name.LocalName, xElement.Value); + else + { + //Debug.LogWarning("Key already found in RawProfileData: " + xElement.Name.LocalName); + //if the key is already in the dict, we will just update the value for consistency. + _fitbitData.RawProfileData[xElement.Name.LocalName] = xElement.Value; + } + + if (_fitbitData.ProfileData.ContainsKey(xElement.Name.LocalName)) + { + _fitbitData.ProfileData[xElement.Name.LocalName] = xElement.Value; + } + } + } + + private void ParseStepsData(string data) + { + //convert the json to xml cause json blows hard. + XmlDocument json = JsonConvert.DeserializeXmlNode(data); + + XDocument doc = XDocument.Parse(json.InnerXml); + var root = doc.Descendants("value").FirstOrDefault(); + _fitbitData.CurrentSteps = ToInt(root.Value); + + Debug.Log("Steps from Fitbit: " + _fitbitData.CurrentSteps); + } + + private void ParseDistanceData(string data) + { + XmlDocument json = JsonConvert.DeserializeXmlNode(data); + + XDocument doc = XDocument.Parse(json.InnerXml); + var root = doc.Descendants("value").FirstOrDefault().Value; + //trim the value + if (root.Length > 4) + root = root.Substring(0, 4); + + _fitbitData.CurrentDistance = ToDouble(root); + + Debug.Log("Distance from Fitbit is:" + _fitbitData.CurrentDistance); + } + + private void ParseCaloriesData(string data) + { + XmlDocument json = JsonConvert.DeserializeXmlNode(data); + + var doc = XDocument.Parse(json.InnerXml); + var calories = doc.Descendants("value").FirstOrDefault().Value; + + _fitbitData.CurrentCalories = ToInt(calories); + } + + private void ParseSleepData(string data) + { + Debug.Log(data); + XmlDocument json = JsonConvert.DeserializeXmlNode(data); + + var doc = XDocument.Parse(json.InnerXml); + var sleepTimeTotal = doc.Descendants("value").FirstOrDefault().Value; + Debug.Log("Minutes asleep for: " + sleepTimeTotal); + + _fitbitData.CurrentSleep = ToInt(sleepTimeTotal); + } + + + #endregion + + static string UppercaseFirst(string s) + { + // Check for empty string. + if (string.IsNullOrEmpty(s)) + { + return string.Empty; + } + // Return char and concat substring. + return char.ToUpper(s[0]) + s.Substring(1); + } + + IEnumerator WaitForAccess(WWW www) + { + Debug.Log("waiting for access\n"); + yield return www; + Debug.Log("Past the Yield \n"); + // check for errors + if (www.error == null) + { + Debug.Log("no error \n"); + Debug.Log("Steps from Fitbit: " + _fitbitData.CurrentSteps); + Debug.Log(www.text); + Debug.Log("Steps from Fitbit: " + _fitbitData.CurrentSteps); + //Debug.Log("WWW Ok!: " + www.text); + // _accessToken = www.responseHeaders["access_token"]; + } + if (www.error != null) + { + Debug.Log("\n Error" + www.error); + Debug.Log(www.error); + } + Debug.Log("end of WaitForAccess \n"); + + } + + //just a utility function to get the correct date format for activity calls that require one + public static string GetCurrentDate() + { + var date = ""; + date += DateTime.Now.Year; + if (DateTime.Now.Month < 10) + { + date += "-" + "0" + DateTime.Now.Month; + } + else + { + date += "-" + DateTime.Now.Month; + } + + if (DateTime.Now.Day < 10) + { + date += "-" + "0" + DateTime.Now.Day; + } + else + { + date += "-" + DateTime.Now.Day; + } + //date += "-" + 15; + return date; + } + + + + private int ToInt(string thing) + { + var temp = 0; + Int32.TryParse(thing, out temp); + return temp; + } + + private double ToDouble(string thing) + { + var temp = 0.0; + Double.TryParse(thing, out temp); + return temp; + } + + } + +} diff --git a/MoCha/Assets/Scripts/FitBitAPI.cs.meta b/MoCha/Assets/Scripts/FitBitAPI.cs.meta new file mode 100644 index 0000000..8514324 --- /dev/null +++ b/MoCha/Assets/Scripts/FitBitAPI.cs.meta @@ -0,0 +1,13 @@ +fileFormatVersion: 2 +guid: aa5e856e9b878764db7c192f3aa20277 +timeCreated: 1524716713 +licenseType: Free +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MoCha/Assets/Scripts/FitbitCaller.cs b/MoCha/Assets/Scripts/FitbitCaller.cs new file mode 100644 index 0000000..0dad878 --- /dev/null +++ b/MoCha/Assets/Scripts/FitbitCaller.cs @@ -0,0 +1,15 @@ +using UnityEngine; +using System.Collections; + +public class FitbitCaller : MonoBehaviour { + + // Use this for initialization + void Start () { + + } + + // Update is called once per frame + void Update () { + + } +} diff --git a/MoCha/Assets/Scripts/FitbitCaller.cs.meta b/MoCha/Assets/Scripts/FitbitCaller.cs.meta new file mode 100644 index 0000000..1dc151a --- /dev/null +++ b/MoCha/Assets/Scripts/FitbitCaller.cs.meta @@ -0,0 +1,13 @@ +fileFormatVersion: 2 +guid: 36444c03eedb84147a326a329de30d0e +timeCreated: 1524716713 +licenseType: Free +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MoCha/Assets/Scripts/FitbitData.cs b/MoCha/Assets/Scripts/FitbitData.cs new file mode 100644 index 0000000..2d7e88b --- /dev/null +++ b/MoCha/Assets/Scripts/FitbitData.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; + +namespace Assets.Scripts.Fitbit +{ + /// + /// Holder class for Fitbit pulled Data. + /// + public class FitbitData + { + public string CurrentTab = "Profile"; + + public Dictionary RawProfileData; + public Dictionary ProfileData; + + public int CurrentSteps; + public int LastSteps; + + public double CurrentDistance; + public double LastDistance; + + public int CurrentCalories; + public int LastCalories; + + public int CurrentSleep; + public int LastSleep; + + public DateTime LastSyncTime; + + public enum summary + { + activityCalories,caloriesBMR,caloriesOut,distances,activityDistance,distance, + elevation,fairlyActiveMinutes,floors,lightlyActiveMinutes,marginalCalories,sedentaryMinutes, + steps,veryActiveMinutes + } + + public FitbitData() + { + RawProfileData =new Dictionary(); + ProfileData = new Dictionary(); + + //we will build the Profile Data Keys that we want so we can compare them later + //to decide what we keep and what we don't when we get the actual data + ProfileData.Add("age",""); + ProfileData.Add("avatar",""); + ProfileData.Add("averageDailySteps",""); + ProfileData.Add("city",""); + ProfileData.Add("country",""); + ProfileData.Add("dateOfBirth",""); + ProfileData.Add("gender",""); + ProfileData.Add("memberSince",""); + } + } +} diff --git a/MoCha/Assets/Scripts/FitbitData.cs.meta b/MoCha/Assets/Scripts/FitbitData.cs.meta new file mode 100644 index 0000000..add5ba3 --- /dev/null +++ b/MoCha/Assets/Scripts/FitbitData.cs.meta @@ -0,0 +1,13 @@ +fileFormatVersion: 2 +guid: 7a928c029cb14204588724261a513ec1 +timeCreated: 1524716713 +licenseType: Free +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MoCha/Assets/Scripts/JSONObject.cs b/MoCha/Assets/Scripts/JSONObject.cs new file mode 100644 index 0000000..c6ede39 --- /dev/null +++ b/MoCha/Assets/Scripts/JSONObject.cs @@ -0,0 +1,1017 @@ +#define PRETTY //Comment out when you no longer need to read JSON to disable pretty Print system-wide +//Using doubles will cause errors in VectorTemplates.cs; Unity speaks floats +#define USEFLOAT //Use floats for numbers instead of doubles (enable if you're getting too many significant digits in string output) +//#define POOLING //Currently using a build setting for this one (also it's experimental) + +using System.Diagnostics; +using UnityEngine; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using Debug = UnityEngine.Debug; + +/* + * http://www.opensource.org/licenses/lgpl-2.1.php + * JSONObject class v.1.4.1 + * for use with Unity + * Copyright Matt Schoen 2010 - 2013 + */ + +public class JSONObject { +#if POOLING + const int MAX_POOL_SIZE = 10000; + public static Queue releaseQueue = new Queue(); +#endif + + const int MAX_DEPTH = 100; + const string INFINITY = "\"INFINITY\""; + const string NEGINFINITY = "\"NEGINFINITY\""; + const string NaN = "\"NaN\""; + public static readonly char[] WHITESPACE = { ' ', '\r', '\n', '\t', '\uFEFF', '\u0009' }; + public enum Type { NULL, STRING, NUMBER, OBJECT, ARRAY, BOOL, BAKED } + public bool isContainer { get { return (type == Type.ARRAY || type == Type.OBJECT); } } + public Type type = Type.NULL; + public int Count { + get { + if(list == null) + return -1; + return list.Count; + } + } + public List list; + public List keys; + public string str; +#if USEFLOAT + public float n; + public float f { + get { + return n; + } + } +#else + public double n; + public float f { + get { + return (float)n; + } + } +#endif + public bool b; + public delegate void AddJSONConents(JSONObject self); + + public static JSONObject nullJO { get { return Create(Type.NULL); } } //an empty, null object + public static JSONObject obj { get { return Create(Type.OBJECT); } } //an empty object + public static JSONObject arr { get { return Create(Type.ARRAY); } } //an empty array + + public JSONObject(Type t) { + type = t; + switch(t) { + case Type.ARRAY: + list = new List(); + break; + case Type.OBJECT: + list = new List(); + keys = new List(); + break; + } + } + public JSONObject(bool b) { + type = Type.BOOL; + this.b = b; + } +#if USEFLOAT + public JSONObject(float f) { + type = Type.NUMBER; + n = f; + } +#else + public JSONObject(double d) { + type = Type.NUMBER; + n = d; + } +#endif + public JSONObject(Dictionary dic) { + type = Type.OBJECT; + keys = new List(); + list = new List(); + //Not sure if it's worth removing the foreach here + foreach(KeyValuePair kvp in dic) { + keys.Add(kvp.Key); + list.Add(CreateStringObject(kvp.Value)); + } + } + public JSONObject(Dictionary dic) { + type = Type.OBJECT; + keys = new List(); + list = new List(); + //Not sure if it's worth removing the foreach here + foreach(KeyValuePair kvp in dic) { + keys.Add(kvp.Key); + list.Add(kvp.Value); + } + } + public JSONObject(AddJSONConents content) { + content.Invoke(this); + } + public JSONObject(JSONObject[] objs) { + type = Type.ARRAY; + list = new List(objs); + } + //Convenience function for creating a JSONObject containing a string. This is not part of the constructor so that malformed JSON data doesn't just turn into a string object + public static JSONObject StringObject(string val) { return CreateStringObject(val); } + public void Absorb(JSONObject obj) { + list.AddRange(obj.list); + keys.AddRange(obj.keys); + str = obj.str; + n = obj.n; + b = obj.b; + type = obj.type; + } + public static JSONObject Create() { +#if POOLING + JSONObject result = null; + while(result == null && releaseQueue.Count > 0) { + result = releaseQueue.Dequeue(); +#if DEV + //The following cases should NEVER HAPPEN (but they do...) + if(result == null) + Debug.Log("wtf " + releaseQueue.Count); + else if(result.list != null) + Debug.Log("wtflist " + result.list.Count); +#endif + } + if(result != null) + return result; +#endif + return new JSONObject(); + } + public static JSONObject Create(Type t) { + JSONObject obj = Create(); + obj.type = t; + switch(t) { + case Type.ARRAY: + obj.list = new List(); + break; + case Type.OBJECT: + obj.list = new List(); + obj.keys = new List(); + break; + } + return obj; + } + public static JSONObject Create(bool val) { + JSONObject obj = Create(); + obj.type = Type.BOOL; + obj.b = val; + return obj; + } + public static JSONObject Create(float val) { + JSONObject obj = Create(); + obj.type = Type.NUMBER; + obj.n = val; + return obj; + } + public static JSONObject Create(int val) { + JSONObject obj = Create(); + obj.type = Type.NUMBER; + obj.n = val; + return obj; + } + public static JSONObject CreateStringObject(string val) { + JSONObject obj = Create(); + obj.type = Type.STRING; + obj.str = val; + return obj; + } + public static JSONObject CreateBakedObject(string val) { + JSONObject bakedObject = Create(); + bakedObject.type = Type.BAKED; + bakedObject.str = val; + return bakedObject; + } + /// + /// Create a JSONObject by parsing string data + /// + /// The string to be parsed + /// The maximum depth for the parser to search. Set this to to 1 for the first level, + /// 2 for the first 2 levels, etc. It defaults to -2 because -1 is the depth value that is parsed (see below) + /// Whether to store levels beyond maxDepth in baked JSONObjects + /// Whether to be strict in the parsing. For example, non-strict parsing will successfully + /// parse "a string" into a string-type + /// + public static JSONObject Create(string val, int maxDepth = -2, bool storeExcessLevels = false, bool strict = false) { + JSONObject obj = Create(); + obj.Parse(val, maxDepth, storeExcessLevels, strict); + return obj; + } + public static JSONObject Create(AddJSONConents content) { + JSONObject obj = Create(); + content.Invoke(obj); + return obj; + } + public static JSONObject Create(Dictionary dic) { + JSONObject obj = Create(); + obj.type = Type.OBJECT; + obj.keys = new List(); + obj.list = new List(); + //Not sure if it's worth removing the foreach here + foreach(KeyValuePair kvp in dic) { + obj.keys.Add(kvp.Key); + obj.list.Add(CreateStringObject(kvp.Value)); + } + return obj; + } + public JSONObject() { } + #region PARSE + public JSONObject(string str, int maxDepth = -2, bool storeExcessLevels = false, bool strict = false) { //create a new JSONObject from a string (this will also create any children, and parse the whole string) + Parse(str, maxDepth, storeExcessLevels, strict); + } + void Parse(string str, int maxDepth = -2, bool storeExcessLevels = false, bool strict = false) { + if(!string.IsNullOrEmpty(str)) { + str = str.Trim(WHITESPACE); + if(strict) { + if(str[0] != '[' && str[0] != '{') { + type = Type.NULL; + Debug.LogWarning("Improper (strict) JSON formatting. First character must be [ or {"); + return; + } + } + if(str.Length > 0) { +#if UNITY_WP8 + if (str == "true") { + type = Type.BOOL; + b = true; + } else if (str == "false") { + type = Type.BOOL; + b = false; + } else if (str == "null") { + type = Type.NULL; +#else + if(string.Compare(str, "true", true) == 0) { + type = Type.BOOL; + b = true; + } else if(string.Compare(str, "false", true) == 0) { + type = Type.BOOL; + b = false; + } else if(string.Compare(str, "null", true) == 0) { + type = Type.NULL; +#endif +#if USEFLOAT + } else if(str == INFINITY) { + type = Type.NUMBER; + n = float.PositiveInfinity; + } else if(str == NEGINFINITY) { + type = Type.NUMBER; + n = float.NegativeInfinity; + } else if(str == NaN) { + type = Type.NUMBER; + n = float.NaN; +#else + } else if(str == INFINITY) { + type = Type.NUMBER; + n = double.PositiveInfinity; + } else if(str == NEGINFINITY) { + type = Type.NUMBER; + n = double.NegativeInfinity; + } else if(str == NaN) { + type = Type.NUMBER; + n = double.NaN; +#endif + } else if(str[0] == '"') { + type = Type.STRING; + this.str = str.Substring(1, str.Length - 2); + } else { + int tokenTmp = 1; + /* + * Checking for the following formatting (www.json.org) + * object - {"field1":value,"field2":value} + * array - [value,value,value] + * value - string - "string" + * - number - 0.0 + * - bool - true -or- false + * - null - null + */ + int offset = 0; + switch(str[offset]) { + case '{': + type = Type.OBJECT; + keys = new List(); + list = new List(); + break; + case '[': + type = Type.ARRAY; + list = new List(); + break; + default: + try { +#if USEFLOAT + n = System.Convert.ToSingle(str); +#else + n = System.Convert.ToDouble(str); +#endif + type = Type.NUMBER; + } catch(System.FormatException) { + type = Type.NULL; + Debug.LogWarning("improper JSON formatting:" + str); + } + return; + } + string propName = ""; + bool openQuote = false; + bool inProp = false; + int depth = 0; + while(++offset < str.Length) { + if(System.Array.IndexOf(WHITESPACE, str[offset]) > -1) + continue; + if(str[offset] == '\\') { + offset += 1; + continue; + } + if(str[offset] == '"') { + if(openQuote) { + if(!inProp && depth == 0 && type == Type.OBJECT) + propName = str.Substring(tokenTmp + 1, offset - tokenTmp - 1); + openQuote = false; + } else { + if(depth == 0 && type == Type.OBJECT) + tokenTmp = offset; + openQuote = true; + } + } + if(openQuote) + continue; + if(type == Type.OBJECT && depth == 0) { + if(str[offset] == ':') { + tokenTmp = offset + 1; + inProp = true; + } + } + + if(str[offset] == '[' || str[offset] == '{') { + depth++; + } else if(str[offset] == ']' || str[offset] == '}') { + depth--; + } + //if (encounter a ',' at top level) || a closing ]/} + if((str[offset] == ',' && depth == 0) || depth < 0) { + inProp = false; + string inner = str.Substring(tokenTmp, offset - tokenTmp).Trim(WHITESPACE); + if(inner.Length > 0) { + if(type == Type.OBJECT) + keys.Add(propName); + if(maxDepth != -1) //maxDepth of -1 is the end of the line + list.Add(Create(inner, (maxDepth < -1) ? -2 : maxDepth - 1)); + else if(storeExcessLevels) + list.Add(CreateBakedObject(inner)); + + } + tokenTmp = offset + 1; + } + } + } + } else type = Type.NULL; + } else type = Type.NULL; //If the string is missing, this is a null + //Profiler.EndSample(); + } + #endregion + public bool IsNumber { get { return type == Type.NUMBER; } } + public bool IsNull { get { return type == Type.NULL; } } + public bool IsString { get { return type == Type.STRING; } } + public bool IsBool { get { return type == Type.BOOL; } } + public bool IsArray { get { return type == Type.ARRAY; } } + public bool IsObject { get { return type == Type.OBJECT || type == Type.BAKED; } } + public void Add(bool val) { + Add(Create(val)); + } + public void Add(float val) { + Add(Create(val)); + } + public void Add(int val) { + Add(Create(val)); + } + public void Add(string str) { + Add(CreateStringObject(str)); + } + public void Add(AddJSONConents content) { + Add(Create(content)); + } + public void Add(JSONObject obj) { + if(obj) { //Don't do anything if the object is null + if(type != Type.ARRAY) { + type = Type.ARRAY; //Congratulations, son, you're an ARRAY now + if(list == null) + list = new List(); + } + list.Add(obj); + } + } + public void AddField(string name, bool val) { + AddField(name, Create(val)); + } + public void AddField(string name, float val) { + AddField(name, Create(val)); + } + public void AddField(string name, int val) { + AddField(name, Create(val)); + } + public void AddField(string name, AddJSONConents content) { + AddField(name, Create(content)); + } + public void AddField(string name, string val) { + AddField(name, CreateStringObject(val)); + } + public void AddField(string name, JSONObject obj) { + if(obj) { //Don't do anything if the object is null + if(type != Type.OBJECT) { + if(keys == null) + keys = new List(); + if(type == Type.ARRAY) { + for(int i = 0; i < list.Count; i++) + keys.Add(i + ""); + } else + if(list == null) + list = new List(); + type = Type.OBJECT; //Congratulations, son, you're an OBJECT now + } + keys.Add(name); + list.Add(obj); + } + } + public void SetField(string name, string val) { SetField(name, CreateStringObject(val)); } + public void SetField(string name, bool val) { SetField(name, Create(val)); } + public void SetField(string name, float val) { SetField(name, Create(val)); } + public void SetField(string name, int val) { SetField(name, Create(val)); } + public void SetField(string name, JSONObject obj) { + if(HasField(name)) { + list.Remove(this[name]); + keys.Remove(name); + } + AddField(name, obj); + } + public void RemoveField(string name) { + if(keys.IndexOf(name) > -1) { + list.RemoveAt(keys.IndexOf(name)); + keys.Remove(name); + } + } + public delegate void FieldNotFound(string name); + public delegate void GetFieldResponse(JSONObject obj); + public bool GetField(ref bool field, string name, bool fallback) { + if (GetField(ref field, name)) { return true; } + field = fallback; + return false; + } + public bool GetField(ref bool field, string name, FieldNotFound fail = null) { + if(type == Type.OBJECT) { + int index = keys.IndexOf(name); + if(index >= 0) { + field = list[index].b; + return true; + } + } + if(fail != null) fail.Invoke(name); + return false; + } +#if USEFLOAT + public bool GetField(ref float field, string name, float fallback) { +#else + public bool GetField(ref double field, string name, double fallback) { +#endif + if (GetField(ref field, name)) { return true; } + field = fallback; + return false; + } +#if USEFLOAT + public bool GetField(ref float field, string name, FieldNotFound fail = null) { +#else + public bool GetField(ref double field, string name, FieldNotFound fail = null) { +#endif + if(type == Type.OBJECT) { + int index = keys.IndexOf(name); + if(index >= 0) { + field = list[index].n; + return true; + } + } + if(fail != null) fail.Invoke(name); + return false; + } + public bool GetField(ref int field, string name, int fallback) { + if (GetField(ref field, name)) { return true; } + field = fallback; + return false; + } + public bool GetField(ref int field, string name, FieldNotFound fail = null) { + if (IsObject) { + int index = keys.IndexOf(name); + if(index >= 0) { + field = (int)list[index].n; + return true; + } + } + if(fail != null) fail.Invoke(name); + return false; + } + public bool GetField(ref uint field, string name, uint fallback) { + if (GetField(ref field, name)) { return true; } + field = fallback; + return false; + } + public bool GetField(ref uint field, string name, FieldNotFound fail = null) { + if (IsObject) { + int index = keys.IndexOf(name); + if(index >= 0) { + field = (uint)list[index].n; + return true; + } + } + if(fail != null) fail.Invoke(name); + return false; + } + public bool GetField(ref string field, string name, string fallback) { + if (GetField(ref field, name)) { return true; } + field = fallback; + return false; + } + public bool GetField(ref string field, string name, FieldNotFound fail = null) { + if (IsObject) { + int index = keys.IndexOf(name); + if(index >= 0) { + field = list[index].str; + return true; + } + } + if(fail != null) fail.Invoke(name); + return false; + } + public void GetField(string name, GetFieldResponse response, FieldNotFound fail = null) { + if(response != null && IsObject) { + int index = keys.IndexOf(name); + if(index >= 0) { + response.Invoke(list[index]); + return; + } + } + if(fail != null) fail.Invoke(name); + } + public JSONObject GetField(string name) { + if (IsObject) + for(int i = 0; i < keys.Count; i++) + if(keys[i] == name) + return list[i]; + return null; + } + public bool HasFields(string[] names) { + if (!IsObject) + return false; + for(int i = 0; i < names.Length; i++) + if(!keys.Contains(names[i])) + return false; + return true; + } + public bool HasField(string name) { + if (!IsObject) + return false; + for (int i = 0; i < keys.Count; i++) + if (keys[i] == name) + return true; + return false; + } + public void Clear() { + type = Type.NULL; + if(list != null) + list.Clear(); + if(keys != null) + keys.Clear(); + str = ""; + n = 0; + b = false; + } + /// + /// Copy a JSONObject. This could probably work better + /// + /// + public JSONObject Copy() { + return Create(Print()); + } + /* + * The Merge function is experimental. Use at your own risk. + */ + public void Merge(JSONObject obj) { + MergeRecur(this, obj); + } + /// + /// Merge object right into left recursively + /// + /// The left (base) object + /// The right (new) object + static void MergeRecur(JSONObject left, JSONObject right) { + if(left.type == Type.NULL) + left.Absorb(right); + else if(left.type == Type.OBJECT && right.type == Type.OBJECT) { + for(int i = 0; i < right.list.Count; i++) { + string key = right.keys[i]; + if(right[i].isContainer) { + if(left.HasField(key)) + MergeRecur(left[key], right[i]); + else + left.AddField(key, right[i]); + } else { + if(left.HasField(key)) + left.SetField(key, right[i]); + else + left.AddField(key, right[i]); + } + } + } else if(left.type == Type.ARRAY && right.type == Type.ARRAY) { + if(right.Count > left.Count) { + Debug.LogError("Cannot merge arrays when right object has more elements"); + return; + } + for(int i = 0; i < right.list.Count; i++) { + if(left[i].type == right[i].type) { //Only overwrite with the same type + if(left[i].isContainer) + MergeRecur(left[i], right[i]); + else { + left[i] = right[i]; + } + } + } + } + } + public void Bake() { + if(type != Type.BAKED) { + str = Print(); + type = Type.BAKED; + } + } + public IEnumerable BakeAsync() { + if(type != Type.BAKED) { + foreach(string s in PrintAsync()) { + if(s == null) + yield return s; + else { + str = s; + } + } + type = Type.BAKED; + } + } +#pragma warning disable 219 + public string Print(bool pretty = false) { + StringBuilder builder = new StringBuilder(); + Stringify(0, builder, pretty); + return builder.ToString(); + } + public IEnumerable PrintAsync(bool pretty = false) { + StringBuilder builder = new StringBuilder(); + printWatch.Reset(); + printWatch.Start(); + foreach(IEnumerable e in StringifyAsync(0, builder, pretty)) { + yield return null; + } + yield return builder.ToString(); + } +#pragma warning restore 219 + #region STRINGIFY + const float maxFrameTime = 0.008f; + static readonly Stopwatch printWatch = new Stopwatch(); + IEnumerable StringifyAsync(int depth, StringBuilder builder, bool pretty = false) { //Convert the JSONObject into a string + //Profiler.BeginSample("JSONprint"); + if(depth++ > MAX_DEPTH) { + Debug.Log("reached max depth!"); + yield break; + } + if(printWatch.Elapsed.TotalSeconds > maxFrameTime) { + printWatch.Reset(); + yield return null; + printWatch.Start(); + } + switch(type) { + case Type.BAKED: + builder.Append(str); + break; + case Type.STRING: + builder.AppendFormat("\"{0}\"", str); + break; + case Type.NUMBER: +#if USEFLOAT + if(float.IsInfinity(n)) + builder.Append(INFINITY); + else if(float.IsNegativeInfinity(n)) + builder.Append(NEGINFINITY); + else if(float.IsNaN(n)) + builder.Append(NaN); +#else + if(double.IsInfinity(n)) + builder.Append(INFINITY); + else if(double.IsNegativeInfinity(n)) + builder.Append(NEGINFINITY); + else if(double.IsNaN(n)) + builder.Append(NaN); +#endif + else + builder.Append(n.ToString()); + break; + case Type.OBJECT: + builder.Append("{"); + if(list.Count > 0) { +#if(PRETTY) //for a bit more readability, comment the define above to disable system-wide + if(pretty) + builder.Append("\n"); +#endif + for(int i = 0; i < list.Count; i++) { + string key = keys[i]; + JSONObject obj = list[i]; + if(obj) { +#if(PRETTY) + if(pretty) + for(int j = 0; j < depth; j++) + builder.Append("\t"); //for a bit more readability +#endif + builder.AppendFormat("\"{0}\":", key); + foreach(IEnumerable e in obj.StringifyAsync(depth, builder, pretty)) + yield return e; + builder.Append(","); +#if(PRETTY) + if(pretty) + builder.Append("\n"); +#endif + } + } +#if(PRETTY) + if(pretty) + builder.Length -= 2; + else +#endif + builder.Length--; + } +#if(PRETTY) + if(pretty && list.Count > 0) { + builder.Append("\n"); + for(int j = 0; j < depth - 1; j++) + builder.Append("\t"); //for a bit more readability + } +#endif + builder.Append("}"); + break; + case Type.ARRAY: + builder.Append("["); + if(list.Count > 0) { +#if(PRETTY) + if(pretty) + builder.Append("\n"); //for a bit more readability +#endif + for(int i = 0; i < list.Count; i++) { + if(list[i]) { +#if(PRETTY) + if(pretty) + for(int j = 0; j < depth; j++) + builder.Append("\t"); //for a bit more readability +#endif + foreach(IEnumerable e in list[i].StringifyAsync(depth, builder, pretty)) + yield return e; + builder.Append(","); +#if(PRETTY) + if(pretty) + builder.Append("\n"); //for a bit more readability +#endif + } + } +#if(PRETTY) + if(pretty) + builder.Length -= 2; + else +#endif + builder.Length--; + } +#if(PRETTY) + if(pretty && list.Count > 0) { + builder.Append("\n"); + for(int j = 0; j < depth - 1; j++) + builder.Append("\t"); //for a bit more readability + } +#endif + builder.Append("]"); + break; + case Type.BOOL: + if(b) + builder.Append("true"); + else + builder.Append("false"); + break; + case Type.NULL: + builder.Append("null"); + break; + } + //Profiler.EndSample(); + } + //TODO: Refactor Stringify functions to share core logic + /* + * I know, I know, this is really bad form. It turns out that there is a + * significant amount of garbage created when calling as a coroutine, so this + * method is duplicated. Hopefully there won't be too many future changes, but + * I would still like a more elegant way to optionaly yield + */ + void Stringify(int depth, StringBuilder builder, bool pretty = false) { //Convert the JSONObject into a string + //Profiler.BeginSample("JSONprint"); + if(depth++ > MAX_DEPTH) { + Debug.Log("reached max depth!"); + return; + } + switch(type) { + case Type.BAKED: + builder.Append(str); + break; + case Type.STRING: + builder.AppendFormat("\"{0}\"", str); + break; + case Type.NUMBER: +#if USEFLOAT + if(float.IsInfinity(n)) + builder.Append(INFINITY); + else if(float.IsNegativeInfinity(n)) + builder.Append(NEGINFINITY); + else if(float.IsNaN(n)) + builder.Append(NaN); +#else + if(double.IsInfinity(n)) + builder.Append(INFINITY); + else if(double.IsNegativeInfinity(n)) + builder.Append(NEGINFINITY); + else if(double.IsNaN(n)) + builder.Append(NaN); +#endif + else + builder.Append(n.ToString()); + break; + case Type.OBJECT: + builder.Append("{"); + if(list.Count > 0) { +#if(PRETTY) //for a bit more readability, comment the define above to disable system-wide + if(pretty) + builder.Append("\n"); +#endif + for(int i = 0; i < list.Count; i++) { + string key = keys[i]; + JSONObject obj = list[i]; + if(obj) { +#if(PRETTY) + if(pretty) + for(int j = 0; j < depth; j++) + builder.Append("\t"); //for a bit more readability +#endif + builder.AppendFormat("\"{0}\":", key); + obj.Stringify(depth, builder, pretty); + builder.Append(","); +#if(PRETTY) + if(pretty) + builder.Append("\n"); +#endif + } + } +#if(PRETTY) + if(pretty) + builder.Length -= 2; + else +#endif + builder.Length--; + } +#if(PRETTY) + if(pretty && list.Count > 0) { + builder.Append("\n"); + for(int j = 0; j < depth - 1; j++) + builder.Append("\t"); //for a bit more readability + } +#endif + builder.Append("}"); + break; + case Type.ARRAY: + builder.Append("["); + if(list.Count > 0) { +#if(PRETTY) + if(pretty) + builder.Append("\n"); //for a bit more readability +#endif + for(int i = 0; i < list.Count; i++) { + if(list[i]) { +#if(PRETTY) + if(pretty) + for(int j = 0; j < depth; j++) + builder.Append("\t"); //for a bit more readability +#endif + list[i].Stringify(depth, builder, pretty); + builder.Append(","); +#if(PRETTY) + if(pretty) + builder.Append("\n"); //for a bit more readability +#endif + } + } +#if(PRETTY) + if(pretty) + builder.Length -= 2; + else +#endif + builder.Length--; + } +#if(PRETTY) + if(pretty && list.Count > 0) { + builder.Append("\n"); + for(int j = 0; j < depth - 1; j++) + builder.Append("\t"); //for a bit more readability + } +#endif + builder.Append("]"); + break; + case Type.BOOL: + if(b) + builder.Append("true"); + else + builder.Append("false"); + break; + case Type.NULL: + builder.Append("null"); + break; + } + //Profiler.EndSample(); + } + #endregion + public static implicit operator WWWForm(JSONObject obj) { + WWWForm form = new WWWForm(); + for(int i = 0; i < obj.list.Count; i++) { + string key = i + ""; + if(obj.type == Type.OBJECT) + key = obj.keys[i]; + string val = obj.list[i].ToString(); + if(obj.list[i].type == Type.STRING) + val = val.Replace("\"", ""); + form.AddField(key, val); + } + return form; + } + public JSONObject this[int index] { + get { + if(list.Count > index) return list[index]; + return null; + } + set { + if(list.Count > index) + list[index] = value; + } + } + public JSONObject this[string index] { + get { + return GetField(index); + } + set { + SetField(index, value); + } + } + public override string ToString() { + return Print(); + } + public string ToString(bool pretty) { + return Print(pretty); + } + public Dictionary ToDictionary() { + if(type == Type.OBJECT) { + Dictionary result = new Dictionary(); + for(int i = 0; i < list.Count; i++) { + JSONObject val = list[i]; + switch(val.type) { + case Type.STRING: result.Add(keys[i], val.str); break; + case Type.NUMBER: result.Add(keys[i], val.n + ""); break; + case Type.BOOL: result.Add(keys[i], val.b + ""); break; + default: Debug.LogWarning("Omitting object: " + keys[i] + " in dictionary conversion"); break; + } + } + return result; + } + Debug.LogWarning("Tried to turn non-Object JSONObject into a dictionary"); + return null; + } + public static implicit operator bool(JSONObject o) { + return o != null; + } +#if POOLING + static bool pool = true; + public static void ClearPool() { + pool = false; + releaseQueue.Clear(); + pool = true; + } + + ~JSONObject() { + if(pool && releaseQueue.Count < MAX_POOL_SIZE) { + type = Type.NULL; + list = null; + keys = null; + str = ""; + n = 0; + b = false; + releaseQueue.Enqueue(this); + } + } +#endif +} \ No newline at end of file diff --git a/MoCha/Assets/Scripts/JSONObject.cs.meta b/MoCha/Assets/Scripts/JSONObject.cs.meta new file mode 100644 index 0000000..c095bab --- /dev/null +++ b/MoCha/Assets/Scripts/JSONObject.cs.meta @@ -0,0 +1,13 @@ +fileFormatVersion: 2 +guid: da64cad21a33a6444ac511376fcb771b +timeCreated: 1524716713 +licenseType: Free +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MoCha/Assets/Scripts/OAuth2AccessToken.cs b/MoCha/Assets/Scripts/OAuth2AccessToken.cs new file mode 100644 index 0000000..e12b10e --- /dev/null +++ b/MoCha/Assets/Scripts/OAuth2AccessToken.cs @@ -0,0 +1,10 @@ +namespace Assets.Scripts +{ + public class OAuth2AccessToken + { + public string Token { get; set; } + public string TokenType { get; set; } // "Bearer" is expected + public int ExpiresIn { get; set; } //maybe convert this to a DateTime ? + public string RefreshToken { get; set; } + } +} \ No newline at end of file diff --git a/MoCha/Assets/Scripts/OAuth2AccessToken.cs.meta b/MoCha/Assets/Scripts/OAuth2AccessToken.cs.meta new file mode 100644 index 0000000..320c6d8 --- /dev/null +++ b/MoCha/Assets/Scripts/OAuth2AccessToken.cs.meta @@ -0,0 +1,13 @@ +fileFormatVersion: 2 +guid: 2e4069fa83cbbb041aa91e83f7ece28e +timeCreated: 1524716713 +licenseType: Free +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MoCha/ProjectSettings/ProjectSettings.asset b/MoCha/ProjectSettings/ProjectSettings.asset index f93fdd1e3d4a106919694408227b23b6d0453e63..d8a42fd8958e31040a217174a9390950cf663793 100644 GIT binary patch literal 56395 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