EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XStopWatch", "test\XStopWatch\XStopWatch.csproj", "{A689445F-79CA-4843-BB47-B39860C863CD}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UnderArmour", "UnderArmour", "{50836253-D1BC-4BCC-B968-52974065F166}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MapMyRun", "MapMyRun", "{7295CB65-4F82-425B-A5CC-5DA241D5F6F1}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MapMyRunService", "MapMyRunService", "{DCF6FC4F-A868-42A2-AEDA-DFD737B670B4}"
+EndProject
+Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "MapMyRun", "test\UnderArmour\MapMyRun\MapMyRun\MapMyRun.shproj", "{3C82C042-F1F4-42E3-B82D-DFED5E06EE19}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MapMyRun.Tizen", "test\UnderArmour\MapMyRun\MapMyRun.Tizen\MapMyRun.Tizen.csproj", "{017E4C5E-4CF8-4A84-898A-5045BF22646C}"
+ ProjectSection(ProjectDependencies) = postProject
+ {4D093123-296A-4471-A97F-58E5F8F2948D} = {4D093123-296A-4471-A97F-58E5F8F2948D}
+ EndProjectSection
+EndProject
+Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "MapMyRunService", "test\UnderArmour\MapMyRunService\MapMyRunService\MapMyRunService.shproj", "{7B89FD19-6DCD-4721-9000-FA612161C492}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MapMyRunService.Tizen", "test\UnderArmour\MapMyRunService\MapMyRunService.Tizen\MapMyRunService.Tizen.csproj", "{09C952B3-BE3F-447E-838F-CD9D14A76D09}"
+EndProject
Global
+ GlobalSection(SharedMSBuildProjectFiles) = preSolution
+ test\UnderArmour\MapMyRun\MapMyRun\MapMyRun.projitems*{017e4c5e-4cf8-4a84-898a-5045bf22646c}*SharedItemsImports = 5
+ test\UnderArmour\MapMyRunService\MapMyRunService\MapMyRunService.projitems*{09c952b3-be3f-447e-838f-cd9d14a76d09}*SharedItemsImports = 5
+ test\UnderArmour\MapMyRun\MapMyRun\MapMyRun.projitems*{3c82c042-f1f4-42e3-b82d-dfed5e06ee19}*SharedItemsImports = 13
+ test\UnderArmour\MapMyRunService\MapMyRunService\MapMyRunService.projitems*{7b89fd19-6dcd-4721-9000-fa612161c492}*SharedItemsImports = 13
+ EndGlobalSection
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
{A689445F-79CA-4843-BB47-B39860C863CD}.Release|x64.Build.0 = Release|Any CPU
{A689445F-79CA-4843-BB47-B39860C863CD}.Release|x86.ActiveCfg = Release|Any CPU
{A689445F-79CA-4843-BB47-B39860C863CD}.Release|x86.Build.0 = Release|Any CPU
+ {017E4C5E-4CF8-4A84-898A-5045BF22646C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {017E4C5E-4CF8-4A84-898A-5045BF22646C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {017E4C5E-4CF8-4A84-898A-5045BF22646C}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {017E4C5E-4CF8-4A84-898A-5045BF22646C}.Debug|x64.Build.0 = Debug|Any CPU
+ {017E4C5E-4CF8-4A84-898A-5045BF22646C}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {017E4C5E-4CF8-4A84-898A-5045BF22646C}.Debug|x86.Build.0 = Debug|Any CPU
+ {017E4C5E-4CF8-4A84-898A-5045BF22646C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {017E4C5E-4CF8-4A84-898A-5045BF22646C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {017E4C5E-4CF8-4A84-898A-5045BF22646C}.Release|x64.ActiveCfg = Release|Any CPU
+ {017E4C5E-4CF8-4A84-898A-5045BF22646C}.Release|x64.Build.0 = Release|Any CPU
+ {017E4C5E-4CF8-4A84-898A-5045BF22646C}.Release|x86.ActiveCfg = Release|Any CPU
+ {017E4C5E-4CF8-4A84-898A-5045BF22646C}.Release|x86.Build.0 = Release|Any CPU
+ {09C952B3-BE3F-447E-838F-CD9D14A76D09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {09C952B3-BE3F-447E-838F-CD9D14A76D09}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {09C952B3-BE3F-447E-838F-CD9D14A76D09}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {09C952B3-BE3F-447E-838F-CD9D14A76D09}.Debug|x64.Build.0 = Debug|Any CPU
+ {09C952B3-BE3F-447E-838F-CD9D14A76D09}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {09C952B3-BE3F-447E-838F-CD9D14A76D09}.Debug|x86.Build.0 = Debug|Any CPU
+ {09C952B3-BE3F-447E-838F-CD9D14A76D09}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {09C952B3-BE3F-447E-838F-CD9D14A76D09}.Release|Any CPU.Build.0 = Release|Any CPU
+ {09C952B3-BE3F-447E-838F-CD9D14A76D09}.Release|x64.ActiveCfg = Release|Any CPU
+ {09C952B3-BE3F-447E-838F-CD9D14A76D09}.Release|x64.Build.0 = Release|Any CPU
+ {09C952B3-BE3F-447E-838F-CD9D14A76D09}.Release|x86.ActiveCfg = Release|Any CPU
+ {09C952B3-BE3F-447E-838F-CD9D14A76D09}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
{7A4D58D3-EF64-46FC-B293-75F71D9938DF} = {BCEBC994-EAB5-4142-B60C-58FED3DFC835}
{198A2943-3091-46FB-B967-B85D26496025} = {BCEBC994-EAB5-4142-B60C-58FED3DFC835}
{A689445F-79CA-4843-BB47-B39860C863CD} = {BCEBC994-EAB5-4142-B60C-58FED3DFC835}
+ {50836253-D1BC-4BCC-B968-52974065F166} = {BCEBC994-EAB5-4142-B60C-58FED3DFC835}
+ {7295CB65-4F82-425B-A5CC-5DA241D5F6F1} = {50836253-D1BC-4BCC-B968-52974065F166}
+ {DCF6FC4F-A868-42A2-AEDA-DFD737B670B4} = {50836253-D1BC-4BCC-B968-52974065F166}
+ {3C82C042-F1F4-42E3-B82D-DFED5E06EE19} = {7295CB65-4F82-425B-A5CC-5DA241D5F6F1}
+ {017E4C5E-4CF8-4A84-898A-5045BF22646C} = {7295CB65-4F82-425B-A5CC-5DA241D5F6F1}
+ {7B89FD19-6DCD-4721-9000-FA612161C492} = {DCF6FC4F-A868-42A2-AEDA-DFD737B670B4}
+ {09C952B3-BE3F-447E-838F-CD9D14A76D09} = {DCF6FC4F-A868-42A2-AEDA-DFD737B670B4}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {4312D28C-6473-4391-B1AF-AAAFAF4FA0C1}
--- /dev/null
+using System;
+using NUnit.Framework;
+using Moq;
+using MapMyRun.Models;
+using System.Collections.Generic;
+
+namespace MapMyRun.Tests
+{
+ [TestFixture]
+ class DispatcherTest
+ {
+ Mock<IMessagePortHandler> mockMessagePortHandler = null;
+
+ [SetUp]
+ public void Setup()
+ {
+ mockMessagePortHandler = new Mock<IMessagePortHandler>();
+ }
+
+ [Test]
+ public void Initialize_Called_CallMessagePortHandlerConnect()
+ {
+ mockMessagePortHandler.Setup(x => x.Connect(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>()));
+
+ var dispatcher = Dispatcher.Instance;
+ dispatcher.Initialize(mockMessagePortHandler.Object);
+
+ mockMessagePortHandler.Verify(x => x.Connect(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>()), Times.Once());
+ }
+
+ [Test]
+ public void Initialize_Called_AddOnReceiveMessage()
+ {
+ var dispatcher = Dispatcher.Instance;
+
+ mockMessagePortHandler.SetupAdd(m => m.MessageReceived += dispatcher.OnReceiveMessage);
+
+ dispatcher.Initialize(mockMessagePortHandler.Object);
+ mockMessagePortHandler.VerifyAdd(m => m.MessageReceived += dispatcher.OnReceiveMessage, Times.Once());
+ }
+
+ [Test]
+ public void SendRequest_FirstCalled_CallMessagePortHandlerSend()
+ {
+ var requestData = "banana";
+ var request = new Mock<Request>(OperationType.PrepareWKO, requestData);
+ Dictionary<string, string> requestDataSet = null;
+
+ mockMessagePortHandler.Setup(x => x.Send(It.IsAny<Dictionary<string, string>>()))
+ .Callback<Dictionary<string, string>>(arg => {
+ requestDataSet = arg;
+ }
+ );
+
+ var dispatcher = Dispatcher.Instance;
+ dispatcher.Initialize(mockMessagePortHandler.Object);
+
+ dispatcher.SendRequest(request.Object);
+ requestDataSet.TryGetValue(Dispatcher.OperationKey, out string operation);
+ requestDataSet.TryGetValue(Dispatcher.DataKey, out string data);
+
+ mockMessagePortHandler.Verify(x => x.Send(It.IsAny<Dictionary<string, string>>()), Times.Once());
+ Assert.AreEqual(((int)OperationType.PrepareWKO).ToString(), operation);
+ Assert.AreEqual(requestData, data);
+ }
+
+ [Test]
+ public void OnReceiveMessage_Called_CallCorrectRequestOnReceiveResponse()
+ {
+ string requestData = "orange";
+ var request = new Request(OperationType.PrepareWKO, requestData);
+ bool isCalled = false;
+ request.ResponseReceived += (Response data) =>
+ {
+ isCalled = true;
+ };
+
+ var dispatcher = Dispatcher.Instance;
+ dispatcher.Initialize(mockMessagePortHandler.Object);
+ var tid = dispatcher.SendRequest(request);
+
+ var responseData = new Dictionary<string, string>
+ {
+ { Dispatcher.TransIdKey, tid}
+ };
+
+ dispatcher.OnReceiveMessage(responseData);
+ Assert.IsTrue(isCalled);
+ }
+
+ [Test]
+ public void OnReceiveMessage_Called_DontCallIncorrectRequestOnReceiveResponse()
+ {
+ string requestData = "orange";
+ var request = new Request(OperationType.PrepareWKO, requestData);
+ var isCalled = false;
+ request.ResponseReceived += (Response data) =>
+ {
+ isCalled = true;
+ };
+
+ var dispatcher = Dispatcher.Instance;
+ dispatcher.Initialize(mockMessagePortHandler.Object);
+ var tid = dispatcher.SendRequest(request);
+
+ var responseData = new Dictionary<string, string>
+ {
+ { Dispatcher.TransIdKey, tid + "Never"}
+ };
+
+ dispatcher.OnReceiveMessage(responseData);
+ Assert.IsFalse(isCalled);
+ }
+
+ [Test]
+ public void OnReceiveMessage_WithOperation_DontCallRequestOnReceiveResponse()
+ {
+ string requestData = "orange";
+ var request = new Request(OperationType.PrepareWKO, requestData);
+ var isCalled = false;
+ request.ResponseReceived += (Response data) =>
+ {
+ isCalled = true;
+ };
+
+ var dispatcher = Dispatcher.Instance;
+ dispatcher.Initialize(mockMessagePortHandler.Object);
+ var tid = dispatcher.SendRequest(request);
+
+ var responseData = new Dictionary<string, string>
+ {
+ { Dispatcher.OperationKey, "kiwi" },
+ { Dispatcher.TransIdKey, tid}
+ };
+
+ dispatcher.OnReceiveMessage(responseData);
+ Assert.IsFalse(isCalled);
+ }
+
+ [Test]
+ public void OnReceiveMessage_WithoutOperation_ProvideCorrectNotification()
+ {
+ var isCalled = false;
+ var mockObserver = new NotificationObserver(OperationType.ApplicationInitialized);
+ mockObserver.NotificationReceived += (string data) =>
+ {
+ isCalled = true;
+ };
+
+ var dispatcher = Dispatcher.Instance;
+ dispatcher.Initialize(mockMessagePortHandler.Object);
+
+ var monitor = dispatcher.Listen(OperationType.ApplicationInitialized);
+ mockObserver.Subscribe(monitor);
+
+ Dictionary<string, string> responseData = new Dictionary<string, string>
+ {
+ { Dispatcher.TransIdKey, "orange"},
+ { Dispatcher.DataKey, "apple"},
+ { Dispatcher.OperationKey, "46"}
+ };
+ dispatcher.OnReceiveMessage(responseData);
+
+ Assert.IsTrue(isCalled);
+ }
+
+ [Test]
+ public void Listen_Called_ReturnCorrectNotificationProvider()
+ {
+ var dispatcher = Dispatcher.Instance;
+ dispatcher.Initialize(mockMessagePortHandler.Object);
+ var notificationMonitor = dispatcher.Listen(OperationType.ApplicationInitialized);
+ Assert.AreEqual(notificationMonitor.Type, OperationType.ApplicationInitialized);
+ }
+
+ }
+}
--- /dev/null
+using System;
+using NUnit.Framework;
+using Moq;
+using MapMyRun.Models;
+using MapMyRun.Models.Settings;
+
+namespace MapMyRun.Tests
+{
+ [TestFixture]
+ public class LoadingTests
+ {
+ Mock<IAppLauncher> mockAppLauncher = null;
+ Mock<IDispatcher> mockDispatcher = null;
+ Mock<IPhoneService> mockPhoneService = null;
+ Mock<IMessagePortHandler> mockMessagePortHandler = null;
+ Mock<ISettingManager> mockSettingManager = null;
+
+ [SetUp]
+ public void Setup()
+ {
+ mockAppLauncher = new Mock<IAppLauncher>();
+ mockDispatcher = new Mock<IDispatcher>();
+ mockPhoneService = new Mock<IPhoneService>();
+ mockMessagePortHandler = new Mock<IMessagePortHandler>();
+ mockSettingManager = new Mock<ISettingManager>();
+ }
+
+ [TearDown]
+ public void Cleanup()
+ {
+ }
+
+ [Test]
+ public void StartLoading_Called_CallIsPlatformSwVersionSupported()
+ {
+ mockPhoneService.Setup(x => x.IsPlatformSwVersionSupported());
+
+ var loading = new Loading(mockAppLauncher.Object, mockMessagePortHandler.Object,
+ mockDispatcher.Object, mockPhoneService.Object, mockSettingManager.Object, "orange");
+
+ Assert.AreEqual(LoadingState.Not_Started, loading.State);
+ loading.StartLoading();
+ mockPhoneService.Verify(x => x.IsPlatformSwVersionSupported(), Times.Once());
+ }
+
+ [Test]
+ public void StartLoading_Called_CallKeepScreenOn()
+ {
+ mockPhoneService.Setup(x => x.KeepScreenOn());
+ mockPhoneService.Setup(x => x.IsPlatformSwVersionSupported()).Returns(true);
+
+ var loading = new Loading(mockAppLauncher.Object, mockMessagePortHandler.Object,
+ mockDispatcher.Object, mockPhoneService.Object, mockSettingManager.Object, "orange");
+
+ Assert.AreEqual(LoadingState.Not_Started, loading.State);
+ loading.StartLoading();
+ mockPhoneService.Verify(x => x.KeepScreenOn(), Times.Once());
+ }
+
+ [Test]
+ public void StartLoading_Called_IsAuthenticated()
+ {
+ mockPhoneService.Setup(x => x.IsAuthenticated(It.IsAny<Action<PhoneApplicationState>>()));
+ mockPhoneService.Setup(x => x.IsPlatformSwVersionSupported()).Returns(true);
+
+ var loading = new Loading(mockAppLauncher.Object, mockMessagePortHandler.Object,
+ mockDispatcher.Object, mockPhoneService.Object, mockSettingManager.Object, "orange");
+
+ Assert.AreEqual(LoadingState.Not_Started, loading.State);
+ loading.StartLoading();
+ mockPhoneService.Verify(x => x.IsAuthenticated(It.IsAny<Action<PhoneApplicationState>>()), Times.Once());
+ }
+
+ [Test]
+ public void StartLoading_Called_CallLaunchServiceApp()
+ {
+ mockAppLauncher.Setup(x => x.Launch("banana"));
+ mockPhoneService.Setup(x => x.IsPlatformSwVersionSupported()).Returns(true);
+
+ var loading = new Loading(mockAppLauncher.Object, mockMessagePortHandler.Object,
+ mockDispatcher.Object, mockPhoneService.Object, mockSettingManager.Object, "banana");
+
+ Assert.AreEqual(LoadingState.Not_Started, loading.State);
+ loading.StartLoading();
+ mockAppLauncher.Verify(x => x.Launch("banana"), Times.Once());
+ }
+
+ [Test]
+ public void StartLoading_NotSupportedPlatform_NotStartedState()
+ {
+ mockAppLauncher.Setup(x => x.Launch("banana")).Throws(new Exception());
+ mockPhoneService.Setup(x => x.IsPlatformSwVersionSupported()).Returns(false);
+
+ var loading = new Loading(mockAppLauncher.Object, mockMessagePortHandler.Object,
+ mockDispatcher.Object, mockPhoneService.Object, mockSettingManager.Object, "banana");
+
+ Assert.AreEqual(LoadingState.Not_Started, loading.State);
+ loading.StartLoading();
+ Assert.AreEqual(LoadingState.Not_Started, loading.State);
+ }
+
+ [Test]
+ public void StartLoading_LaunchThrowException_ConnectState()
+ {
+ mockAppLauncher.Setup(x => x.Launch("banana")).Throws(new Exception());
+ mockPhoneService.Setup(x => x.IsPlatformSwVersionSupported()).Returns(true);
+
+ var loading = new Loading(mockAppLauncher.Object, mockMessagePortHandler.Object,
+ mockDispatcher.Object, mockPhoneService.Object, mockSettingManager.Object, "banana");
+
+ Assert.AreEqual(LoadingState.Not_Started, loading.State);
+ loading.StartLoading();
+ Assert.AreEqual(LoadingState.Connect, loading.State);
+ }
+
+ [Test]
+ public void StartLoading_Called_CallDispatcherInitialize()
+ {
+ mockAppLauncher.Setup(x => x.Launch("banana"));
+ mockDispatcher.Setup(x => x.Initialize(It.IsAny<IMessagePortHandler>()));
+ mockPhoneService.Setup(x => x.IsPlatformSwVersionSupported()).Returns(true);
+
+ var loading = new Loading(mockAppLauncher.Object, mockMessagePortHandler.Object,
+ mockDispatcher.Object, mockPhoneService.Object, mockSettingManager.Object, "banana");
+
+ Assert.AreEqual(LoadingState.Not_Started, loading.State);
+ loading.StartLoading();
+ mockDispatcher.Verify(x => x.Initialize(It.IsAny<IMessagePortHandler>()), Times.Once());
+ }
+
+ [Test]
+ public void StartLoading_AuthFail_AuthState()
+ {
+ mockPhoneService.Setup(x => x.IsPlatformSwVersionSupported()).Returns(true);
+ mockPhoneService.Setup(x => x.IsAuthenticated(It.IsAny<Action<PhoneApplicationState>>()))
+ .Callback<Action<PhoneApplicationState>>(callback => callback(PhoneApplicationState.NotLoggedIn));
+
+ var loading = new Loading(mockAppLauncher.Object, mockMessagePortHandler.Object,
+ mockDispatcher.Object, mockPhoneService.Object, mockSettingManager.Object, "banana");
+
+ Assert.AreEqual(LoadingState.Not_Started, loading.State);
+ loading.StartLoading();
+ Assert.AreEqual(LoadingState.Auth, loading.State);
+ }
+
+ [Test]
+ public void StartLoading_AuthSuccess_FinishedState()
+ {
+ mockDispatcher.Setup(x => x.Listen(OperationType.Logout)).Returns(new NotificationProvider(OperationType.Logout));
+ mockPhoneService.Setup(x => x.IsPlatformSwVersionSupported()).Returns(true);
+ mockPhoneService.Setup(x => x.IsAuthenticated(It.IsAny<Action<PhoneApplicationState>>()))
+ .Callback<Action<PhoneApplicationState>>(callback => callback(PhoneApplicationState.LoggedIn));
+
+ var loading = new Loading(mockAppLauncher.Object, mockMessagePortHandler.Object,
+ mockDispatcher.Object, mockPhoneService.Object, mockSettingManager.Object, "banana");
+
+ Assert.AreEqual(LoadingState.Not_Started, loading.State);
+ loading.StartLoading();
+ Assert.AreEqual(LoadingState.Finished, loading.State);
+ }
+
+ [Test]
+ public void StartLoading_AuthSuccess_PhoneStateLoggedIn()
+ {
+ mockDispatcher.Setup(x => x.Listen(OperationType.Logout)).Returns(new NotificationProvider(OperationType.Logout));
+ mockPhoneService.Setup(x => x.IsPlatformSwVersionSupported()).Returns(true);
+ mockPhoneService.Setup(x => x.IsAuthenticated(It.IsAny<Action<PhoneApplicationState>>()))
+ .Callback<Action<PhoneApplicationState>>(callback => callback(PhoneApplicationState.LoggedIn));
+
+ var loading = new Loading(mockAppLauncher.Object, mockMessagePortHandler.Object,
+ mockDispatcher.Object, mockPhoneService.Object, mockSettingManager.Object, "banana");
+
+ Assert.AreEqual(PhoneApplicationState.NotSupportedTizenVersion, loading.phoneState);
+ loading.StartLoading();
+ Assert.AreEqual(PhoneApplicationState.LoggedIn, loading.phoneState);
+ }
+
+ [Test]
+ public void StartLoading_AuthSuccess_CallInitializeApp()
+ {
+ mockDispatcher.Setup(x => x.Listen(OperationType.Logout)).Returns(new NotificationProvider(OperationType.Logout));
+ mockPhoneService.Setup(x => x.IsPlatformSwVersionSupported()).Returns(true);
+ mockPhoneService.Setup(x => x.InitializeApp());
+ mockPhoneService.Setup(x => x.IsAuthenticated(It.IsAny<Action<PhoneApplicationState>>()))
+ .Callback<Action<PhoneApplicationState>>(callback => callback(PhoneApplicationState.LoggedIn));
+
+ var loading = new Loading(mockAppLauncher.Object, mockMessagePortHandler.Object,
+ mockDispatcher.Object, mockPhoneService.Object, mockSettingManager.Object, "banana");
+
+ Assert.AreEqual(PhoneApplicationState.NotSupportedTizenVersion, loading.phoneState);
+ loading.StartLoading();
+ mockPhoneService.Verify(x => x.InitializeApp(), Times.Once());
+ }
+
+ [Test]
+ public void InitializeLogoutEventHandler_Called_CallDispatcherListenWithLogoutType()
+ {
+ var loading = new Loading(mockAppLauncher.Object, mockMessagePortHandler.Object,
+ mockDispatcher.Object, mockPhoneService.Object, mockSettingManager.Object, "banana");
+ mockDispatcher.Setup(x => x.Listen(OperationType.Logout)).Returns(new NotificationProvider(OperationType.Logout));
+ loading.InitializeLogoutEventHandler();
+
+ mockDispatcher.Verify(x => x.Listen(OperationType.Logout), Times.Once());
+ }
+
+ [Test]
+ public void InitializeAfterLoading_Called_CallPhoneServiceOnAppStart()
+ {
+ mockDispatcher.Setup(x => x.Listen(OperationType.Logout)).Returns(new NotificationProvider(OperationType.Logout));
+ mockPhoneService.Setup(x => x.OnAppStart());
+
+ var loading = new Loading(mockAppLauncher.Object, mockMessagePortHandler.Object,
+ mockDispatcher.Object, mockPhoneService.Object, mockSettingManager.Object, "banana");
+ loading.InitializeAfterLoading();
+
+ mockPhoneService.Verify(x => x.OnAppStart(), Times.Once());
+ }
+
+ [Test]
+ public void InitializeAfterLoading_Called_CallSendRequestWithScontext()
+ {
+ mockDispatcher.Setup(x => x.Listen(OperationType.Logout)).Returns(new NotificationProvider(OperationType.Logout));
+ Request request = null;
+ mockDispatcher.Setup(x => x.SendRequest(It.IsAny<Request>())).Callback<Request>(arg => {
+ if (arg.Operation == OperationType.CheckSContext)
+ request = arg;
+ }
+ );
+ mockPhoneService.Setup(x => x.OnAppStart());
+
+ var loading = new Loading(mockAppLauncher.Object, mockMessagePortHandler.Object,
+ mockDispatcher.Object, mockPhoneService.Object, mockSettingManager.Object, "banana");
+ loading.InitializeAfterLoading();
+
+ mockDispatcher.Verify(x => x.SendRequest(It.IsAny<Request>()), Times.AtLeastOnce());
+ Assert.AreEqual(request.Operation, OperationType.CheckSContext);
+ Assert.IsTrue(request.Data.Equals(""));
+ }
+
+ [Test]
+ public void InitializeAfterLoading_Called_CallSendRequestWithGpsAcquire()
+ {
+ mockDispatcher.Setup(x => x.Listen(OperationType.Logout)).Returns(new NotificationProvider(OperationType.Logout));
+ Request request = null;
+ mockDispatcher.Setup(x => x.SendRequest(It.IsAny<Request>())).Callback<Request>(arg => {
+ if (arg.Operation == OperationType.StartGpsAcquire)
+ request = arg;
+ }
+ );
+ mockPhoneService.Setup(x => x.OnAppStart());
+
+ var loading = new Loading(mockAppLauncher.Object, mockMessagePortHandler.Object,
+ mockDispatcher.Object, mockPhoneService.Object, mockSettingManager.Object, "banana");
+ loading.InitializeAfterLoading();
+
+ mockDispatcher.Verify(x => x.SendRequest(It.IsAny<Request>()), Times.AtLeastOnce());
+ Assert.AreEqual(request.Operation, OperationType.StartGpsAcquire);
+ Assert.IsTrue(request.Data.Equals(""));
+ }
+
+ [Test]
+ public void InitializeAfterLoading_Called_CallSendRequestWithTrainingPlan()
+ {
+ mockDispatcher.Setup(x => x.Listen(OperationType.Logout)).Returns(new NotificationProvider(OperationType.Logout));
+ Request request = null;
+ mockDispatcher.Setup(x => x.SendRequest(It.IsAny<Request>())).Callback<Request>(arg => {
+ if (arg.Operation == OperationType.GetTodayTrainingPlans)
+ request = arg;
+ }
+ );
+ mockPhoneService.Setup(x => x.OnAppStart());
+
+ var loading = new Loading(mockAppLauncher.Object, mockMessagePortHandler.Object,
+ mockDispatcher.Object, mockPhoneService.Object, mockSettingManager.Object, "banana");
+ loading.InitializeAfterLoading();
+
+ mockDispatcher.Verify(x => x.SendRequest(It.IsAny<Request>()), Times.AtLeastOnce());
+ Assert.AreEqual(request.Operation, OperationType.GetTodayTrainingPlans);
+ Assert.IsTrue(request.Data.Equals(""));
+ }
+ }
+}
--- /dev/null
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netcoreapp3.1</TargetFramework>
+
+ <IsPackable>false</IsPackable>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Moq" Version="4.13.1" />
+ <PackageReference Include="nunit" Version="3.12.0" />
+ <PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
+ </ItemGroup>
+
+ <Import Project="..\MapMyRun\MapMyRun.projitems" Label="Shared" />
+
+</Project>
--- /dev/null
+using System;
+using NUnit.Framework;
+using Moq;
+using MapMyRun.Models;
+
+namespace MapMyRun.Tests
+{
+ [TestFixture]
+ class PermissionManagerTest
+ {
+ Mock<IDispatcher> mockDispatcher;
+
+ [SetUp]
+ public void Setup()
+ {
+ mockDispatcher = new Mock<IDispatcher>();
+ }
+
+ [Test]
+ public void RequestPermission_Called_CallSendRequest()
+ {
+ Request request = null;
+ mockDispatcher.Setup(x => x.SendRequest(It.IsAny<Request>()))
+ .Callback<Request>(arg =>
+ {
+ request = arg;
+ }
+ );
+
+ var permissionManager = new PermissionManager(mockDispatcher.Object);
+ permissionManager.RequestPermission(PermissionType.Location, null);
+
+ mockDispatcher.Verify(x => x.SendRequest(It.IsAny<Request>()), Times.AtLeastOnce());
+ Assert.AreEqual(request.Operation, OperationType.RequestPermission);
+ }
+ }
+}
--- /dev/null
+using System;
+using NUnit.Framework;
+using Moq;
+using MapMyRun.Models;
+
+namespace MapMyRun.Tests
+{
+ [TestFixture]
+ class PhoneServiceTest
+ {
+ [SetUp]
+ public void Setup()
+ {
+ }
+
+ [Test]
+ public void IsPlatformSwVersionSupported_SupportVersion_ReturnTrue()
+ {
+ var mockPlatformService = new Mock<IPlatformService>();
+ var mockDispatcher = new Mock<IDispatcher>();
+ var supportVersionA = "R360BPK5";
+ var supportVersionB = "R360BPK6";
+ var supportVersionC = "R360CPK5";
+ var supportVersionD = "R380BPK3";
+
+ var phoneService = new PhoneService(mockDispatcher.Object, mockPlatformService.Object);
+
+ mockPlatformService.Setup(x => x.GetSwVersion(out supportVersionA));
+ Assert.AreEqual(true, phoneService.IsPlatformSwVersionSupported());
+
+ mockPlatformService.Setup(x => x.GetSwVersion(out supportVersionB));
+ Assert.AreEqual(true, phoneService.IsPlatformSwVersionSupported());
+
+ mockPlatformService.Setup(x => x.GetSwVersion(out supportVersionC));
+ Assert.AreEqual(true, phoneService.IsPlatformSwVersionSupported());
+
+ mockPlatformService.Setup(x => x.GetSwVersion(out supportVersionD));
+ Assert.AreEqual(true, phoneService.IsPlatformSwVersionSupported());
+ }
+
+ [Test]
+ public void IsPlatformSwVersionSupported_NotSupportVersion_ReturnFalse()
+ {
+ var mockPlatformService = new Mock<IPlatformService>();
+ var mockDispatcher = new Mock<IDispatcher>();
+ var notSupportVersionA = "R360BPK4";
+ var notSupportVersionB = "R360APK5";
+ var notSupportVersionC = "R3601PK5";
+ var notSupportVersionD = "R360BOK5";
+
+ var phoneService = new PhoneService(mockDispatcher.Object, mockPlatformService.Object);
+
+ mockPlatformService.Setup(x => x.GetSwVersion(out notSupportVersionA));
+ Assert.AreEqual(false, phoneService.IsPlatformSwVersionSupported());
+
+ mockPlatformService.Setup(x => x.GetSwVersion(out notSupportVersionB));
+ Assert.AreEqual(false, phoneService.IsPlatformSwVersionSupported());
+
+ mockPlatformService.Setup(x => x.GetSwVersion(out notSupportVersionC));
+ Assert.AreEqual(false, phoneService.IsPlatformSwVersionSupported());
+
+ mockPlatformService.Setup(x => x.GetSwVersion(out notSupportVersionD));
+ Assert.AreEqual(false, phoneService.IsPlatformSwVersionSupported());
+ }
+
+ [Test]
+ public void KeepScreenOn_Called_CallKeepScreenOn()
+ {
+ var mockPlatformService = new Mock<IPlatformService>();
+ var mockDispatcher = new Mock<IDispatcher>();
+ mockPlatformService.Setup(x => x.KeepScreenOn());
+
+ var phoneService = new PhoneService(mockDispatcher.Object, mockPlatformService.Object);
+
+ phoneService.KeepScreenOn();
+ mockPlatformService.Verify(x => x.KeepScreenOn(), Times.Once());
+ }
+
+ [Test]
+ public void IsAuthenticated_Called_CallSendRequest()
+ {
+ var mockPlatformService = new Mock<IPlatformService>();
+ var mockDispatcher = new Mock<IDispatcher>();
+ Request request = null;
+ mockDispatcher.Setup(x => x.SendRequest(It.IsAny<Request>())).Callback<Request>(arg => request = arg);
+
+ var phoneService = new PhoneService(mockDispatcher.Object, mockPlatformService.Object);
+
+ phoneService.IsAuthenticated(It.IsAny<Action<PhoneApplicationState>>());
+ mockDispatcher.Verify(x => x.SendRequest(It.IsAny<Request>()), Times.Once());
+ Assert.AreEqual(request.Operation, OperationType.CheckIsAuthenticated);
+ Assert.IsTrue(request.Data.Equals("{ \"authenticate\": false }"));
+ }
+
+ [Test]
+ public void InitializeApp_Called_CallSendRequest()
+ {
+ var mockPlatformService = new Mock<IPlatformService>();
+ var mockDispatcher = new Mock<IDispatcher>();
+ mockPlatformService.Setup(x => x.KeepScreenOn());
+ Request request = null;
+ mockDispatcher.Setup(x => x.SendRequest(It.IsAny<Request>())).Callback<Request>(arg => request = arg);
+
+ var phoneService = new PhoneService(mockDispatcher.Object, mockPlatformService.Object);
+
+ phoneService.InitializeApp();
+ mockDispatcher.Verify(x => x.SendRequest(It.IsAny<Request>()), Times.Once());
+ Assert.AreEqual(request.Operation, OperationType.ApplicationInitialized);
+ Assert.IsTrue(request.Data.Equals(""));
+ }
+
+ [Test]
+ public void OnAppStart_Called_CallSendRequest()
+ {
+ var mockPlatformService = new Mock<IPlatformService>();
+ var mockDispatcher = new Mock<IDispatcher>();
+ Request request = null;
+ mockDispatcher.Setup(x => x.SendRequest(It.IsAny<Request>())).Callback<Request>(arg => request = arg);
+
+ var phoneService = new PhoneService(mockDispatcher.Object, mockPlatformService.Object);
+
+ phoneService.OnAppStart();
+ mockDispatcher.Verify(x => x.SendRequest(It.IsAny<Request>()), Times.Once());
+ Assert.AreEqual(request.Operation, OperationType.ApplicationStarted);
+ Assert.IsTrue(request.Data.Equals(""));
+ }
+
+ }
+}
--- /dev/null
+using System;
+using NUnit.Framework;
+using Moq;
+using MapMyRun.Models.Workout;
+using System.Collections.Generic;
+using MapMyRun.Models;
+using MapMyRun.Models.Settings;
+
+namespace MapMyRun.Tests
+{
+ [TestFixture]
+ class SettingManagerTest
+ {
+ Mock<IDispatcher> mockDispatcher = null;
+
+ [SetUp]
+ public void Setup()
+ {
+ mockDispatcher = new Mock<IDispatcher>();
+ }
+
+ [Test]
+ public void Initialize_Called_CallSendRequestWithGetSettings()
+ {
+ mockDispatcher.Setup(x => x.Listen(OperationType.GpsStateChanged)).Returns(new NotificationProvider(OperationType.GpsStateChanged));
+ Request request = null;
+ mockDispatcher.Setup(x => x.SendRequest(It.IsAny<Request>())).Callback<Request>(arg => {
+ if (arg.Operation == OperationType.GetSettings)
+ request = arg;
+ }
+ );
+
+ var settingManager = new SettingManager(mockDispatcher.Object);
+ settingManager.Initialize(null);
+
+ mockDispatcher.Verify(x => x.SendRequest(It.IsAny<Request>()), Times.AtLeastOnce());
+ Assert.AreEqual(request.Operation, OperationType.GetSettings);
+ Assert.IsTrue(request.Data.Equals(""));
+ }
+
+ [Test]
+ public void Initialize_Called_DontCallNullDelegate()
+ {
+ mockDispatcher.Setup(x => x.Listen(OperationType.GpsStateChanged)).Returns(new NotificationProvider(OperationType.GpsStateChanged));
+ Request request = null;
+ mockDispatcher.Setup(x => x.SendRequest(It.IsAny<Request>())).Callback<Request>(arg => {
+ if (arg.Operation == OperationType.GetSettings)
+ request = arg;
+ }
+ );
+
+ var settingManager = new SettingManager(mockDispatcher.Object);
+ settingManager.Initialize(null);
+ Assert.IsNotNull(request);
+ request.OnReceiveResponse(new Response("0", "", ResponseStatus.Success));
+ }
+
+ [Test]
+ public void Initialize_Called_CallDelegate()
+ {
+ mockDispatcher.Setup(x => x.Listen(OperationType.GpsStateChanged)).Returns(new NotificationProvider(OperationType.GpsStateChanged));
+ Request request = null;
+ var isCalled = false;
+ mockDispatcher.Setup(x => x.SendRequest(It.IsAny<Request>())).Callback<Request>(arg => {
+ if (arg.Operation == OperationType.GetSettings)
+ request = arg;
+ }
+ );
+
+ var settingManager = new SettingManager(mockDispatcher.Object);
+ settingManager.Initialize(() =>
+ {
+ isCalled = true;
+ });
+ Assert.IsNotNull(request);
+ request.OnReceiveResponse(new Response("0", "", ResponseStatus.Success));
+ Assert.IsTrue(isCalled);
+ }
+
+ [Test]
+ public void Initialize_CalledObserver_SetCorrectState()
+ {
+ var notificationProvider = new NotificationProvider(OperationType.GpsStateChanged);
+ mockDispatcher.Setup(x => x.Listen(OperationType.GpsStateChanged)).Returns(notificationProvider);
+ Request request = null;
+ mockDispatcher.Setup(x => x.SendRequest(It.IsAny<Request>())).Callback<Request>(arg => {
+ if (arg.Operation == OperationType.GetSettings)
+ request = arg;
+ }
+ );
+
+ var settingManager = new SettingManager(mockDispatcher.Object);
+ settingManager.Initialize(null);
+ Assert.IsTrue(settingManager.GpsState == GPSState.Off);
+ request.OnReceiveResponse(new Response("0",
+ "{\"gps\":{\"isEnabled\":true,\"isSupported\":true,\"systemValue\":true}}", ResponseStatus.Success));
+ Assert.IsTrue(settingManager.GpsState == GPSState.Ready);
+ notificationProvider.PostNotification("1");
+ Assert.IsTrue(settingManager.GpsState == GPSState.Acquired);
+ notificationProvider.PostNotification("0");
+ Assert.IsTrue(settingManager.GpsState == GPSState.Acquiring);
+ }
+
+ }
+}
--- /dev/null
+using System;
+using NUnit.Framework;
+using Moq;
+using MapMyRun.Models.Workout;
+using System.Collections.Generic;
+using MapMyRun.Models;
+using MapMyRun.Models.Workout.Items;
+
+namespace MapMyRun.Tests
+{
+ [TestFixture]
+ class WorkoutTest
+ {
+ Mock<IDispatcher> mockDispatcher = null;
+ Mock<IPermissionManager> mockPermissionManager = null;
+
+ const string CaloriesKey = "calories";
+ const string WillpowerKey = "willpower";
+ const string HrZoneKey = "hrZone";
+ const string IntensityKey = "intensity";
+ const string AggregatesKey = "aggregates";
+ const string CadenceKey = "cadence";
+ const string HrKey = "hrData";
+ const string SpeedKey = "speed";
+ const string StrideLengthKey = "strideLength";
+ const string FormCoachKey = "formCoach";
+ const string GoalCoachKey = "speedCoach";
+ const string SpeedCoachKey = "goalCoach";
+
+ [SetUp]
+ public void Setup()
+ {
+ mockDispatcher = new Mock<IDispatcher>();
+ mockPermissionManager = new Mock<IPermissionManager>();
+ }
+
+ [Test]
+ public void PrepareWorkout_Called_CallRequestPermissionWithSensor()
+ {
+ var workout = new Workout(mockDispatcher.Object, mockPermissionManager.Object);
+ mockPermissionManager.Setup(x => x.RequestPermission(PermissionType.Sensor, It.IsAny<OnPermissionResponse>()));
+
+ workout.PrepareWorkout(50, true, true, true);
+
+ mockPermissionManager.Verify(x => x.RequestPermission(PermissionType.Sensor, It.IsAny<OnPermissionResponse>()), Times.Once());
+ }
+
+ [Test]
+ public void PrepareWorkout_CalledWithGpsNeededTrue_CallRequestPermissionWithLocation()
+ {
+ var workout = new Workout(mockDispatcher.Object, mockPermissionManager.Object);
+ OnPermissionResponse onResponse = null;
+ mockPermissionManager.Setup(x => x.RequestPermission(PermissionType.Sensor, It.IsAny<OnPermissionResponse>()))
+ .Callback<PermissionType ,OnPermissionResponse>((type, cb) =>
+ {
+ onResponse = cb;
+ }
+ );
+ mockPermissionManager.Setup(x => x.RequestPermission(PermissionType.Location, It.IsAny<OnPermissionResponse>()));
+
+ workout.PrepareWorkout(50, true, true, true);
+ onResponse(true, true);
+
+ mockPermissionManager.Verify(x => x.RequestPermission(PermissionType.Sensor, It.IsAny<OnPermissionResponse>()), Times.Once());
+ mockPermissionManager.Verify(x => x.RequestPermission(PermissionType.Location, It.IsAny<OnPermissionResponse>()), Times.Once());
+ }
+
+ [Test]
+ public void PrepareWorkout_CalledWithGpsNeededFalse_DontCallRequestPermissionWithLocation()
+ {
+ var workout = new Workout(mockDispatcher.Object, mockPermissionManager.Object);
+ OnPermissionResponse onResponse = null;
+ mockPermissionManager.Setup(x => x.RequestPermission(PermissionType.Sensor, It.IsAny<OnPermissionResponse>()))
+ .Callback<PermissionType, OnPermissionResponse>((type, cb) =>
+ {
+ onResponse = cb;
+ }
+ );
+ mockPermissionManager.Setup(x => x.RequestPermission(PermissionType.Location, It.IsAny<OnPermissionResponse>()));
+
+ workout.PrepareWorkout(50, false, true, true);
+ onResponse(true, true);
+
+ mockPermissionManager.Verify(x => x.RequestPermission(PermissionType.Sensor, It.IsAny<OnPermissionResponse>()), Times.Once());
+ mockPermissionManager.Verify(x => x.RequestPermission(PermissionType.Location, It.IsAny<OnPermissionResponse>()), Times.Never());
+ }
+
+ [Test]
+ public void PrepareWorkout_Called_CallDispatcherSendRequest()
+ {
+ var workout = new Workout(mockDispatcher.Object, mockPermissionManager.Object);
+ OnPermissionResponse onResponse = null;
+ mockPermissionManager.Setup(x => x.RequestPermission(PermissionType.Sensor, It.IsAny<OnPermissionResponse>()))
+ .Callback<PermissionType, OnPermissionResponse>((type, cb) =>
+ {
+ onResponse = cb;
+ }
+ );
+ Request request = null;
+ mockDispatcher.Setup(x => x.SendRequest(It.IsAny<Request>())).Callback<Request>(arg =>
+ {
+ request = arg;
+ }
+ );
+
+ workout.PrepareWorkout(50, false, true, true);
+ onResponse(false, false);
+
+ mockDispatcher.Verify(x => x.SendRequest(It.IsAny<Request>()), Times.Once());
+ Assert.AreEqual(OperationType.PrepareWKO, request.Operation);
+ }
+
+ [Test]
+ public void StartWorkout_Called_CallDispatcherSendRequest()
+ {
+ var workout = new Workout(mockDispatcher.Object, mockPermissionManager.Object);
+ OnPermissionResponse onResponse = null;
+ mockPermissionManager.Setup(x => x.RequestPermission(PermissionType.Sensor, It.IsAny<OnPermissionResponse>()))
+ .Callback<PermissionType, OnPermissionResponse>((type, cb) =>
+ {
+ onResponse = cb;
+ }
+ );
+ Request request = null;
+ mockDispatcher.Setup(x => x.SendRequest(It.IsAny<Request>())).Callback<Request>(arg =>
+ {
+ request = arg;
+ }
+ );
+
+ workout.PrepareWorkout(50, false, true, true);
+ onResponse(false, false);
+ request.OnReceiveResponse(new Response("0", "banana", ResponseStatus.Success));
+ workout.StartWorkout();
+
+ mockDispatcher.Verify(x => x.SendRequest(It.IsAny<Request>()), Times.Exactly(2));
+ Assert.AreEqual(OperationType.StartWKO, request.Operation);
+ }
+
+ [Test]
+ public void StartListenWorkoutData_Called_CallDispatcherListen()
+ {
+ var workout = new Workout(mockDispatcher.Object, mockPermissionManager.Object);
+ mockDispatcher.Setup(x => x.Listen(OperationType.WkoDataChanged)).Returns(new NotificationProvider(OperationType.WkoDataChanged));
+
+ workout.StartListenWorkoutData();
+
+ mockDispatcher.Verify(x => x.Listen(OperationType.WkoDataChanged), Times.Once());
+ }
+
+ [Test]
+ public void PropagateWorkoutData_Called_CallCorrectEvent()
+ {
+ var workout = new Workout(mockDispatcher.Object, mockPermissionManager.Object);
+ mockDispatcher.Setup(x => x.Listen(OperationType.WkoDataChanged)).Returns(new NotificationProvider(OperationType.WkoDataChanged));
+
+ var calledEvents = new List<string>();
+ workout.CaloriesDataReceived += (x) => { calledEvents.Add(CaloriesKey); };
+ workout.WillpowerDataReceived += (x) => { calledEvents.Add(WillpowerKey); };
+ workout.HrZoneDataReceived += (x) => { calledEvents.Add(HrZoneKey); };
+ workout.IntensityDataReceived += (x) => { calledEvents.Add(IntensityKey); };
+ workout.AggregatesDataReceived += (x) => { calledEvents.Add(AggregatesKey); };
+ workout.CadenceDataReceived += (x) => { calledEvents.Add(CadenceKey); };
+ workout.HrDataReceived += (x) => { calledEvents.Add(HrKey); };
+ workout.SpeedDataReceived += (x) => { calledEvents.Add(SpeedKey); };
+ workout.StrideLengthDataReceived += (x) => { calledEvents.Add(StrideLengthKey); };
+ workout.FormCoachDataReceived += (x) => { calledEvents.Add(FormCoachKey); };
+ workout.GoalCoachDataReceived += (x) => { calledEvents.Add(GoalCoachKey); };
+ workout.SpeedCoachDataReceived += (x) => { calledEvents.Add(SpeedCoachKey); };
+
+ Dictionary<string, string> workEventTestData = new Dictionary<string, string>()
+ {
+ { CaloriesKey, "{\"calories\":198}" },
+ { WillpowerKey, "{\"willpower\": \"willpower\"}" },
+ { HrZoneKey, "{\"hrZone\": \"banana\"}" },
+ { IntensityKey, "{\"intensity\": \"orange\"}" },
+ { AggregatesKey, "{\"aggregates\": { \"distance_total\":0,\"metabolic_energy_total\":0 } }" },
+ { CadenceKey, "{\"cadence\": { \"min\":0,\"max\":0 } }" },
+ { HrKey, "{\"hrData\": { \"min\":0,\"max\":0 } }" },
+ { SpeedKey, "{\"speed\": { \"min\":0,\"max\":0 } }" },
+ { StrideLengthKey, "{\"strideLength\": { \"min\":0,\"max\":0 } }" },
+ { FormCoachKey, "{\"formCoach\": { \"targetMin\":0,\"targetMax\":0 } }" },
+ { GoalCoachKey, "{\"goalCoach\": { \"targetMin\":0,\"targetMax\":0 } }" },
+ { SpeedCoachKey, "{\"speedCoach\": { \"current\":0,\"target\":0 } }" }
+ };
+
+ foreach (var entry in workEventTestData)
+ {
+ Console.WriteLine($"Key {entry.Key}");
+ calledEvents.Clear();
+ WorkoutData workoutData = workout.ParseWorkoutDataJson(entry.Value);
+ workout.PropagateWorkoutData(workoutData);
+ Assert.AreEqual(1, calledEvents.Count);
+ Assert.AreEqual(entry.Key, calledEvents[0]);
+ }
+ }
+
+ [Test]
+ public void PropagateWorkoutData_Called_CallMultiCorrectEvent()
+ {
+ var workout = new Workout(mockDispatcher.Object, mockPermissionManager.Object);
+ mockDispatcher.Setup(x => x.Listen(OperationType.WkoDataChanged)).Returns(new NotificationProvider(OperationType.WkoDataChanged));
+
+ var calledEvents = new List<string>();
+ workout.CaloriesDataReceived += (x) => { calledEvents.Add(CaloriesKey); };
+ workout.WillpowerDataReceived += (x) => { calledEvents.Add(WillpowerKey); };
+ workout.HrZoneDataReceived += (x) => { calledEvents.Add(HrZoneKey); };
+ workout.IntensityDataReceived += (x) => { calledEvents.Add(IntensityKey); };
+ workout.AggregatesDataReceived += (x) => { calledEvents.Add(AggregatesKey); };
+ workout.CadenceDataReceived += (x) => { calledEvents.Add(CadenceKey); };
+ workout.HrDataReceived += (x) => { calledEvents.Add(HrKey); };
+ workout.SpeedDataReceived += (x) => { calledEvents.Add(SpeedKey); };
+ workout.StrideLengthDataReceived += (x) => { calledEvents.Add(StrideLengthKey); };
+ workout.FormCoachDataReceived += (x) => { calledEvents.Add(FormCoachKey); };
+ workout.GoalCoachDataReceived += (x) => { calledEvents.Add(GoalCoachKey); };
+ workout.SpeedCoachDataReceived += (x) => { calledEvents.Add(SpeedCoachKey); };
+
+ var calorieWillpowerIntensity = "{\"calories\":198, \"willpower\": \"willpower\", \"intensity\": \"orange\"}";
+ var cadenceSpeedcoachGoal = "{\"cadence\": { \"min\":0,\"max\":0 }, \"speedCoach\": { \"current\":0,\"target\":0 }, \"goalCoach\": { \"targetMin\":0,\"targetMax\":0 } }";
+ var aggregateSpeedHr = "{\"aggregates\": { \"distance_total\":0,\"metabolic_energy_total\":0 }, \"hrData\": { \"min\":0,\"max\":0 }, \"speed\": { \"min\":0,\"max\":0 } }";
+ var hrZoneStrideForm = "{\"hrZone\": \"banana\", \"strideLength\": { \"min\":0,\"max\":0 }, \"formCoach\": { \"targetMin\":0,\"targetMax\":0 } }";
+
+ calledEvents.Clear();
+ WorkoutData workoutData = workout.ParseWorkoutDataJson(calorieWillpowerIntensity);
+ workout.PropagateWorkoutData(workoutData);
+ Assert.AreEqual(3, calledEvents.Count);
+ Assert.AreEqual(CaloriesKey, calledEvents[0]);
+ Assert.AreEqual(WillpowerKey, calledEvents[1]);
+ Assert.AreEqual(IntensityKey, calledEvents[2]);
+
+ calledEvents.Clear();
+ workoutData = workout.ParseWorkoutDataJson(cadenceSpeedcoachGoal);
+ workout.PropagateWorkoutData(workoutData);
+ Assert.AreEqual(3, calledEvents.Count);
+ Assert.AreEqual(CadenceKey, calledEvents[0]);
+ Assert.AreEqual(GoalCoachKey, calledEvents[1]);
+ Assert.AreEqual(SpeedCoachKey, calledEvents[2]);
+
+ calledEvents.Clear();
+ workoutData = workout.ParseWorkoutDataJson(aggregateSpeedHr);
+ workout.PropagateWorkoutData(workoutData);
+ Assert.AreEqual(3, calledEvents.Count);
+ Assert.AreEqual(HrKey, calledEvents[0]);
+ Assert.AreEqual(AggregatesKey, calledEvents[1]);
+ Assert.AreEqual(SpeedKey, calledEvents[2]);
+
+ calledEvents.Clear();
+ workoutData = workout.ParseWorkoutDataJson(hrZoneStrideForm);
+ workout.PropagateWorkoutData(workoutData);
+ Assert.AreEqual(3, calledEvents.Count);
+ Assert.AreEqual(HrZoneKey, calledEvents[0]);
+ Assert.AreEqual(StrideLengthKey, calledEvents[1]);
+ Assert.AreEqual(FormCoachKey, calledEvents[2]);
+ }
+
+ [Test]
+ public void AddItem_Called_AddedCorrectItem()
+ {
+ var workout = new Workout(mockDispatcher.Object, mockPermissionManager.Object);
+
+ workout.AddItem(WorkoutItemType.Calories);
+ var result = workout.GetItem(WorkoutItemType.Calories);
+
+ Assert.IsNotNull(result);
+ }
+
+ [Test]
+ public void GetItem_Called_ReturnNull()
+ {
+ var workout = new Workout(mockDispatcher.Object, mockPermissionManager.Object);
+
+ var result = workout.GetItem(WorkoutItemType.Calories);
+
+ Assert.IsNull(result);
+ }
+
+ [Test]
+ public void ClearAllWorkoutItem_Called_ClearAllItems()
+ {
+ var workout = new Workout(mockDispatcher.Object, mockPermissionManager.Object);
+
+ workout.AddItem(WorkoutItemType.Calories);
+ workout.AddItem(WorkoutItemType.PeakHR);
+ Assert.IsNotNull(workout.GetItem(WorkoutItemType.Calories));
+ Assert.IsNotNull(workout.GetItem(WorkoutItemType.PeakHR));
+
+ workout.ClearAllWorkoutItem();
+
+ Assert.IsNull(workout.GetItem(WorkoutItemType.Calories));
+ Assert.IsNull(workout.GetItem(WorkoutItemType.PeakHR));
+ }
+
+ }
+}
--- /dev/null
+<?xml version="1.0" encoding="utf-8" ?>
+<Application xmlns="http://xamarin.com/schemas/2014/forms"
+ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+ x:Class="MapMyRun.Tizen.App">
+ <Application.Resources>
+
+ <!-- Application resource dictionary -->
+ <ResourceDictionary>
+ <Color x:Key="PrimaryHeaderColor">#F8F8F9</Color>
+ <Color x:Key="PrimaryTextColor">#F8F8F9</Color>
+
+ <Color x:Key="PrimaryCellLabelColor">#F8F8F9</Color>
+ <Color x:Key="DetailCellLabelColor">#008DFF</Color>
+
+ <!--List Styling-->
+ <Style x:Key="PrimaryHeaderStyle" TargetType="Label">
+ <Setter Property="HorizontalTextAlignment" Value="Center" />
+ <Setter Property="VerticalTextAlignment" Value="Center" />
+ <Setter Property="TextColor" Value="{StaticResource PrimaryHeaderColor}" />
+ <Setter Property="FontSize" Value="Small" />
+ </Style>
+ <Style x:Key="PrimaryCellLabelStyle" TargetType="Label">
+ <Setter Property="Margin" Value="0, 20, 0, 0" />
+ <Setter Property="HorizontalTextAlignment" Value="Center" />
+ <Setter Property="VerticalTextAlignment" Value="Center" />
+ <Setter Property="TextColor" Value="{StaticResource PrimaryCellLabelColor}" />
+ <Setter Property="FontSize" Value="Small" />
+ </Style>
+ <Style x:Key="DetailCellLabelStyle" TargetType="Label">
+ <Setter Property="Margin" Value="0, 0, 0, 20" />
+ <Setter Property="HorizontalTextAlignment" Value="Center" />
+ <Setter Property="VerticalTextAlignment" Value="Center" />
+ <Setter Property="TextColor" Value="{StaticResource DetailCellLabelColor}" />
+ <Setter Property="FontSize" Value="Micro" />
+ </Style>
+
+ <Style x:Key="DetailCellLabelStyleCloserMargin" TargetType="Label">
+ <Setter Property="Margin" Value="0, -15, 0, 20" />
+ <Setter Property="HorizontalTextAlignment" Value="Center" />
+ <Setter Property="VerticalTextAlignment" Value="Center" />
+ <Setter Property="TextColor" Value="{StaticResource DetailCellLabelColor}" />
+ <Setter Property="FontSize" Value="Micro" />
+ </Style>
+ </ResourceDictionary>
+ </Application.Resources>
+</Application>
\ No newline at end of file
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Xamarin.Forms;
+using Xamarin.Forms.Xaml;
+using MapMyRun.Models;
+using MapMyRun.Tizen.Models;
+using MapMyRun.Tizen.Views;
+using MapMyRun.Models.Workout;
+using MapMyRun.Models.Settings;
+
+namespace MapMyRun.Tizen
+{
+ [XamlCompilation(XamlCompilationOptions.Compile)]
+ public partial class App : Application
+ {
+ private const string ServiceAppId = "org.tizen.example.MapMyRunService";
+ //private const string ServiceAppId = "org.tizen.mapmyrunservice";
+ private readonly Loading loading;
+ public Workout workout;
+ public SettingManager settingManager;
+ public App()
+ {
+ InitializeComponent();
+ var dispatcher = MapMyRun.Models.Dispatcher.Instance;
+ settingManager = new SettingManager(dispatcher);
+ loading = new Loading(new AppLauncher(), new MessagePortHandler(),
+ dispatcher, new PhoneService(dispatcher, new TizenPlatformService()), settingManager, ServiceAppId);
+ MainPage = new LoadingPage(loading);
+ workout = new Workout(dispatcher, new PermissionManager(dispatcher));
+ workout.StartListenWorkoutData();
+ }
+
+ protected override void OnStart()
+ {
+ Console.WriteLine("OnStart!========================");
+ loading.StartLoading();
+ }
+
+ protected override void OnSleep()
+ {
+ Console.WriteLine("OnSleep!========================");
+ // Handle when your app sleeps
+ }
+
+ protected override void OnResume()
+ {
+ Console.WriteLine("OnResume!========================");
+ // Handle when your app resumes
+ }
+ }
+}
+
--- /dev/null
+<Project Sdk="Tizen.NET.Sdk/1.0.9">
+
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>tizen40</TargetFramework>
+ </PropertyGroup>
+
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugType>portable</DebugType>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>None</DebugType>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
+ </ItemGroup>
+
+ <Import Project="..\MapMyRun\MapMyRun.projitems" Label="Shared" />
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\..\..\src\XSF\XSF.csproj" />
+ </ItemGroup>
+
+</Project>
+
--- /dev/null
+using System;
+using Xamarin.Forms;
+
+namespace MapMyRun.Tizen
+{
+ class Program : global::Xamarin.Forms.Platform.Tizen.FormsApplication
+ {
+ protected override void OnCreate()
+ {
+ base.OnCreate();
+
+ LoadApplication(new App());
+ }
+
+ static void Main(string[] args)
+ {
+ var app = new Program();
+ Forms.Init(app);
+ global::Tizen.Wearable.CircularUI.Forms.Renderer.FormsCircularUI.Init();
+ app.Run(args);
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+using Xamarin.Forms;
+using Tizen.Applications;
+using MapMyRun.Models;
+using MapMyRun.Tizen.Models;
+
+namespace MapMyRun.Tizen.Models
+{
+ public class AppLauncher : IAppLauncher
+ {
+ public void Launch(string appId)
+ {
+ var control = new AppControl()
+ {
+ ApplicationId = appId
+ };
+
+ AppControl.SendLaunchRequest(control);
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+using Tizen.Applications;
+using Tizen.Applications.Messages;
+using MapMyRun.Models;
+using Xamarin.Forms;
+
+namespace MapMyRun.Tizen.Models
+{
+ public class MessagePortHandler : IMessagePortHandler
+ {
+ private string _localPortName = "";
+ private string _remotePortName = "";
+ private string _remoteAppId = "";
+ private bool _isSecureMode = false;
+
+ private bool _isInit = false;
+ private bool _isPendingMessageSending = false;
+ private List<Dictionary<string, string>> _pendingMessages = new List<Dictionary<string, string>>();
+
+ private MessagePort messagePort;
+
+ public event Action<Dictionary<string, string>> MessageReceived = delegate { };
+
+ public void Connect(string localPort, string remoteAppId, string remotePort, bool secureMode)
+ {
+ _localPortName = localPort;
+ _remotePortName = remotePort;
+ _remoteAppId = remoteAppId;
+ _isSecureMode = secureMode;
+
+ messagePort = new MessagePort(_localPortName, _isSecureMode);
+ messagePort.MessageReceived += OnReceive;
+ messagePort.Listen();
+ }
+
+ public void Disconnect()
+ {
+ _localPortName = "";
+ _remotePortName = "";
+ _remoteAppId = "";
+ _isSecureMode = false;
+ messagePort.StopListening();
+ messagePort.Dispose();
+ messagePort = null;
+ }
+
+ public void Send(Dictionary<string, string> data)
+ {
+ if (!_isInit)
+ {
+ _pendingMessages.Add(data);
+
+ if (!_isPendingMessageSending)
+ {
+ _isPendingMessageSending = true;
+ TrySendFirstMessage();
+ }
+ return;
+ }
+
+ SendMessage(data);
+ }
+
+ private void TrySendFirstMessage()
+ {
+ try
+ {
+ var remotePort = new RemotePort(_remoteAppId, _remotePortName, false);
+ bool isRunning = remotePort.IsRunning();
+ Console.WriteLine($"Remote Port: {_remoteAppId}, {_remotePortName}, {isRunning}");
+
+ if (!isRunning)
+ {
+ StartRetrySendFirstMessage();
+ return;
+ }
+
+ foreach (Dictionary<string, string> pendingMessage in _pendingMessages)
+ {
+ //TODO : Fail case, Exit? or Recovery?
+ SendMessage(pendingMessage);
+ }
+
+ _isPendingMessageSending = false;
+ _isInit = true;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine("Exception - Message: " + ex.Message + ", source: " + ex.Source + ", " + ex.StackTrace);
+ StartRetrySendFirstMessage();
+ }
+ }
+
+ void SendMessage(Dictionary<string, string> data)
+ {
+ var bundle = new Bundle();
+ Console.WriteLine($"________________________________");
+ Console.WriteLine($">>>>> [UI] Send Data: ");
+ foreach (var pair in data)
+ {
+ bundle.AddItem(pair.Key, pair.Value);
+ Console.WriteLine($"{{ Key: {pair.Key}, Value: {pair.Value} }}");
+ }
+
+ try
+ {
+ messagePort.Send(bundle, _remoteAppId, _remotePortName, _isSecureMode);
+ Console.WriteLine($"Message sent: {_remoteAppId}, {_remotePortName}, {_isSecureMode}");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine("Exception - Message: " + ex.Message + ", source: " + ex.Source + ", " + ex.StackTrace);
+ }
+ }
+
+ private void StartRetrySendFirstMessage()
+ {
+ Device.StartTimer(TimeSpan.FromSeconds(0.3), () =>
+ {
+ TrySendFirstMessage();
+ return false;
+ });
+ }
+
+ private void OnReceive(object sender, MessageReceivedEventArgs e)
+ {
+ StringBuilder messageLog = new StringBuilder();
+
+ Console.WriteLine($"________________________________");
+ Console.WriteLine("UI application received a message");
+ Console.WriteLine($"App ID: {e.Remote.AppId}");
+ Console.WriteLine($"PortName: {e.Remote.PortName}");
+ Console.WriteLine($"Trusted: {e.Remote.Trusted}");
+ Bundle responseBundle = e.Message;
+
+ Console.WriteLine($"[UI] Response Data <<<<< : ");
+ var reponse = new Dictionary<string, string>();
+
+ foreach (string key in responseBundle.Keys)
+ {
+ if (responseBundle.TryGetItem(key, out string value))
+ {
+ Console.WriteLine($"{{ Key: {key}, Value: {value} }}");
+ reponse.Add(key, value);
+ }
+ }
+
+ MessageReceived(reponse);
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using MapMyRun.Models;
+
+namespace MapMyRun.Tizen.Models
+{
+ class TizenPlatformService : IPlatformService
+ {
+ public void GetSwVersion(out string version)
+ {
+ global::Tizen.System.Information.TryGetValue("http://tizen.org/system/build.string", out version);
+ Console.WriteLine($"SW Version {version}");
+ }
+
+ public void KeepScreenOn()
+ {
+ // TODO : Check it API Version 5
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+
+namespace MapMyRun.Tizen.ViewModels
+{
+ public class BasePageModel : INotifyPropertyChanged
+ {
+ protected bool SetProperty<T>(ref T backingStore, T value,
+ [CallerMemberName]string propertyName = "",
+ Action onChanged = null)
+ {
+ if (EqualityComparer<T>.Default.Equals(backingStore, value))
+ {
+ return false;
+ }
+
+ backingStore = value;
+ onChanged?.Invoke();
+ OnPropertyChanged(propertyName);
+ return true;
+ }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+using MapMyRun.Models;
+namespace MapMyRun.Tizen.ViewModels
+{
+ class LoadingPageModel : BasePageModel
+ {
+ private double _progress;
+ private Loading _loadingModel;
+
+ public double Progress
+ {
+ get
+ {
+ return _progress;
+ }
+
+ set
+ {
+ if (_progress >= 1)
+ return;
+
+ double changedValue = value;
+ if (value >= 1)
+ {
+ changedValue = 1;
+ }
+ SetProperty(ref _progress, changedValue, "Progress");
+ }
+ }
+
+ public LoadingPageModel(Loading model)
+ {
+ _loadingModel = model;
+ _loadingModel.LoadingStateChanged += (state) =>
+ {
+ Progress = (double)state / Enum.GetValues(typeof(LoadingState)).Length;
+ };
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.ComponentModel;
+using MapMyRun.Models.Settings;
+using Xamarin.Forms;
+using MapMyRun.Models;
+
+namespace MapMyRun.Tizen.ViewModels
+{
+ public class MainPageModel : BasePageModel
+ {
+ private SettingManager settingManager;
+
+ private GPSState _gpsState;
+ public GPSState GpsState
+ {
+ get
+ {
+ return _gpsState;
+ }
+ set
+ {
+ SetProperty(ref _gpsState, value, "GpsState");
+
+ switch (value)
+ {
+ case GPSState.Acquired:
+ GpsStatusImg = ImageSource.FromFile("icon-gps--connected.png");
+ break;
+ case GPSState.Ready:
+ case GPSState.Acquiring:
+ GpsStatusImg = ImageSource.FromFile("icon-gps--connecting.png");
+ break;
+ case GPSState.DisabledForActivity:
+ GpsStatusImg = ImageSource.FromFile("icon-gps--not-connected.png");
+ break;
+ case GPSState.Off:
+ GpsStatusImg = ImageSource.FromFile("icon-gps--not-connected.png");
+ break;
+ default:
+ GpsStatusImg = ImageSource.FromFile("icon-gps--error.png");
+ break;
+ }
+ }
+ }
+
+ private ImageSource _gpsStatusImg;
+ public ImageSource GpsStatusImg
+ {
+ get
+ {
+ return _gpsStatusImg;
+ }
+ set
+ {
+ SetProperty(ref _gpsStatusImg, value, "GpsStatusImg");
+ }
+ }
+
+ public MainPageModel()
+ {
+ settingManager = ((App)Application.Current).settingManager;
+ settingManager.GpsStateChanged += (GPSState nextGpsState) =>
+ {
+ GpsState = nextGpsState;
+ };
+ GpsState = settingManager.GpsState;
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.ComponentModel;
+using Xamarin.Forms;
+using MapMyRun.Models.Settings;
+
+namespace MapMyRun.Tizen.ViewModels
+{
+ public class SettingsPageModel : BasePageModel
+ {
+ private SettingManager settingManager;
+ private DisplayTimeType _displayTime { get; set; }
+
+ public int DisplayTime {
+ get {
+ return (int) _displayTime;
+ }
+ set {
+ if (Enum.IsDefined(typeof(DisplayTimeType), value))
+ {
+ _displayTime = (DisplayTimeType)value;
+ }
+ }
+ }
+
+
+ private bool gpsStatus;
+ public bool GPSStatus
+ {
+ get
+ {
+ return gpsStatus;
+ }
+ set
+ {
+ gpsStatus = value;
+ settingManager.ToggleGPS(gpsStatus);
+ }
+ }
+
+ private static SettingsPageModel _instance;
+
+ public static SettingsPageModel GetInstance()
+ {
+ if (_instance == null)
+ {
+ _instance = new SettingsPageModel();
+ }
+ return _instance;
+ }
+
+ private SettingsPageModel()
+ {
+ // Insert loading of settings here.
+ settingManager = ((App)Application.Current).settingManager;
+ _displayTime = DisplayTimeType.longDisplayTime;
+ GPSStatus = false;
+ }
+ }
+
+ public enum DisplayTimeType
+ {
+ longDisplayTime = 16,
+ midDisplayTime = 12,
+ shortDisplayTime = 6
+ }
+}
--- /dev/null
+using MapMyRun.Models.Workout.Items;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Timers;
+
+namespace MapMyRun.Tizen.ViewModels.WorkoutMain
+{
+ class AverageHrViewModel : BasePageModel, IWorkoutItemViewModel
+ {
+ private string _val;
+ private string _valWithUnit;
+ public string Value
+ {
+ get { return _val; }
+ set
+ {
+ string nextValue = String.Equals(value, "0", StringComparison.OrdinalIgnoreCase) ? Item.GetEmptyValue() : value;
+
+ SetProperty(ref _val, nextValue);
+ ValueWithUnit = nextValue;
+ }
+ }
+
+ public string ValueWithUnit
+ {
+ get { return _valWithUnit; }
+ set
+ {
+ SetProperty(ref _valWithUnit, value + Item.GetUnit());
+ }
+ }
+
+ public string LongLabel
+ {
+ get { return Item.GetLabel(WorkoutItemLabelType.LongLabel); }
+ }
+
+ public string MediumLabel
+ {
+ get { return Item.GetLabel(WorkoutItemLabelType.MediumLabel); }
+ }
+
+ public string ShortLabel
+ {
+ get { return Item.GetLabel(WorkoutItemLabelType.ShortLabel); }
+ }
+
+ public WorkoutItemType ItemType { get; set; }
+ public IWorkoutItem Item { get; set; }
+
+ public AverageHrViewModel(IWorkoutItem item)
+ {
+ ItemType = WorkoutItemType.AvgHR;
+ Item = item;
+ Value = item.GetEmptyValue();
+ }
+
+ public void Initialize()
+ {
+ Console.WriteLine("AverageHr Initialize!");
+
+ var item = (AverageHr)Item;
+ item.ValueChanged += (x) =>
+ {
+ Console.WriteLine($"AverageHr Changed : {x}");
+ Value = x;
+ };
+ }
+ }
+}
--- /dev/null
+using MapMyRun.Models.Workout.Items;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Timers;
+
+namespace MapMyRun.Tizen.ViewModels.WorkoutMain
+{
+ class CadenceViewModel : BasePageModel, IWorkoutItemViewModel
+ {
+ private string _val;
+ private string _valWithUnit;
+ public string Value
+ {
+ get { return _val; }
+ set
+ {
+ SetProperty(ref _val, value);
+ ValueWithUnit = value;
+ }
+ }
+
+ public string ValueWithUnit
+ {
+ get { return _valWithUnit; }
+ set
+ {
+ SetProperty(ref _valWithUnit, value + Item.GetUnit());
+ }
+ }
+
+ public string LongLabel
+ {
+ get { return Item.GetLabel(WorkoutItemLabelType.LongLabel); }
+ }
+
+ public string MediumLabel
+ {
+ get { return Item.GetLabel(WorkoutItemLabelType.MediumLabel); }
+ }
+
+ public string ShortLabel
+ {
+ get { return Item.GetLabel(WorkoutItemLabelType.ShortLabel); }
+ }
+
+ public WorkoutItemType ItemType { get; set; }
+ public IWorkoutItem Item { get; set; }
+
+ public CadenceViewModel(IWorkoutItem item)
+ {
+ ItemType = WorkoutItemType.Cadence;
+ Item = item;
+ Value = item.GetEmptyValue();
+ }
+
+ public void Initialize()
+ {
+ Console.WriteLine("CurrentCadence Initialize!");
+
+ var item = (CurrentCadence)Item;
+ item.ValueChanged += (x) =>
+ {
+ Console.WriteLine($"CurrentCadence Changed : {x}");
+ Value = x;
+ };
+ }
+ }
+}
--- /dev/null
+using MapMyRun.Models.Workout.Items;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Timers;
+
+namespace MapMyRun.Tizen.ViewModels.WorkoutMain
+{
+ class CaloriesViewModel : BasePageModel, IWorkoutItemViewModel
+ {
+ private string _val;
+ private string _valWithUnit;
+ public string Value
+ {
+ get { return _val; }
+ set
+ {
+ SetProperty(ref _val, value);
+ ValueWithUnit = value;
+ }
+ }
+
+ public string ValueWithUnit
+ {
+ get { return _valWithUnit; }
+ set
+ {
+ SetProperty(ref _valWithUnit, value + Item.GetUnit());
+ }
+ }
+
+ public string LongLabel
+ {
+ get { return Item.GetLabel(WorkoutItemLabelType.LongLabel); }
+ }
+
+ public string MediumLabel
+ {
+ get { return Item.GetLabel(WorkoutItemLabelType.MediumLabel); }
+ }
+
+ public string ShortLabel
+ {
+ get { return Item.GetLabel(WorkoutItemLabelType.ShortLabel); }
+ }
+
+ public WorkoutItemType ItemType { get; set; }
+ public IWorkoutItem Item { get; set; }
+
+ public CaloriesViewModel(IWorkoutItem item)
+ {
+ ItemType = WorkoutItemType.Calories;
+ Item = item;
+ Value = item.GetEmptyValue();
+ }
+
+ public void Initialize()
+ {
+ Console.WriteLine("Calorie Initialize!");
+
+ var item = (Calories)Item;
+ item.ValueChanged += (x) =>
+ {
+ Console.WriteLine($"Calories Changed : {x}");
+ Value = x;
+ };
+ }
+ }
+}
--- /dev/null
+using MapMyRun.Models.Workout.Items;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Timers;
+
+namespace MapMyRun.Tizen.ViewModels.WorkoutMain
+{
+ class CurrentHrViewModel : BasePageModel, IWorkoutItemViewModel
+ {
+ private string _val;
+ private string _valWithUnit;
+ public string Value
+ {
+ get { return _val; }
+ set
+ {
+ string nextValue = String.Equals(value, "0", StringComparison.OrdinalIgnoreCase) ? Item.GetEmptyValue() : value;
+
+ SetProperty(ref _val, nextValue);
+ ValueWithUnit = nextValue;
+ }
+ }
+
+ public string ValueWithUnit
+ {
+ get { return _valWithUnit; }
+ set
+ {
+ SetProperty(ref _valWithUnit, value + Item.GetUnit());
+ }
+ }
+
+ public string LongLabel
+ {
+ get { return Item.GetLabel(WorkoutItemLabelType.LongLabel); }
+ }
+
+ public string MediumLabel
+ {
+ get { return Item.GetLabel(WorkoutItemLabelType.MediumLabel); }
+ }
+
+ public string ShortLabel
+ {
+ get { return Item.GetLabel(WorkoutItemLabelType.ShortLabel); }
+ }
+
+ public WorkoutItemType ItemType { get; set; }
+ public IWorkoutItem Item { get; set; }
+
+ public CurrentHrViewModel(IWorkoutItem item)
+ {
+ ItemType = WorkoutItemType.CurrentHR;
+ Item = item;
+ Value = item.GetEmptyValue();
+ }
+
+ public void Initialize()
+ {
+ Console.WriteLine("CurrentHr Initialize!");
+
+ var item = (CurrentHr)Item;
+ item.ValueChanged += (x) =>
+ {
+ Console.WriteLine($"CurrentHr Changed : {x}");
+ Value = x;
+ };
+ }
+ }
+}
--- /dev/null
+using MapMyRun.Models.Workout.Items;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Timers;
+
+namespace MapMyRun.Tizen.ViewModels.WorkoutMain
+{
+ class CurrentPaceViewModel : BasePageModel, IWorkoutItemViewModel
+ {
+ private string _val;
+ private string _valWithUnit;
+ public string Value
+ {
+ get { return _val; }
+ set
+ {
+ SetProperty(ref _val, value);
+ ValueWithUnit = value;
+ }
+ }
+
+ public string ValueWithUnit
+ {
+ get { return _valWithUnit; }
+ set
+ {
+ SetProperty(ref _valWithUnit, value + Item.GetUnit());
+ }
+ }
+
+ public string LongLabel
+ {
+ get { return Item.GetLabel(WorkoutItemLabelType.LongLabel); }
+ }
+
+ public string MediumLabel
+ {
+ get { return Item.GetLabel(WorkoutItemLabelType.MediumLabel); }
+ }
+
+ public string ShortLabel
+ {
+ get { return Item.GetLabel(WorkoutItemLabelType.ShortLabel); }
+ }
+
+ public WorkoutItemType ItemType { get; set; }
+ public IWorkoutItem Item { get; set; }
+
+ public CurrentPaceViewModel(IWorkoutItem item)
+ {
+ ItemType = WorkoutItemType.Pace;
+ Item = item;
+ Value = item.GetEmptyValue();
+ }
+
+ public void Initialize()
+ {
+ Console.WriteLine("CurrentPace Initialize!");
+
+ var item = (CurrentPace)Item;
+ item.ValueChanged += (x) =>
+ {
+ Console.WriteLine($"CurrentPace Changed : {x}");
+ Value = x;
+ };
+ }
+ }
+}
--- /dev/null
+using MapMyRun.Models.Workout.Items;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Timers;
+
+namespace MapMyRun.Tizen.ViewModels.WorkoutMain
+{
+ class DistanceViewModel : BasePageModel, IWorkoutItemViewModel
+ {
+ private string _val;
+ private string _valWithUnit;
+ public string Value
+ {
+ get { return _val; }
+ set
+ {
+ SetProperty(ref _val, value);
+ ValueWithUnit = value;
+ }
+ }
+
+ public string ValueWithUnit
+ {
+ get { return _valWithUnit; }
+ set
+ {
+ SetProperty(ref _valWithUnit, value + Item.GetUnit());
+ }
+ }
+
+ public string LongLabel
+ {
+ get { return Item.GetLabel(WorkoutItemLabelType.LongLabel); }
+ }
+
+ public string MediumLabel
+ {
+ get { return Item.GetLabel(WorkoutItemLabelType.MediumLabel); }
+ }
+
+ public string ShortLabel
+ {
+ get { return Item.GetLabel(WorkoutItemLabelType.ShortLabel); }
+ }
+
+ public WorkoutItemType ItemType { get; set; }
+ public IWorkoutItem Item { get; set; }
+
+ public DistanceViewModel(IWorkoutItem item)
+ {
+ ItemType = WorkoutItemType.Distance;
+ Item = item;
+ Value = item.GetEmptyValue();
+ }
+
+ public void Initialize()
+ {
+ Console.WriteLine("Distance Initialize!");
+
+ var item = (Distance)Item;
+ item.ValueChanged += (x) =>
+ {
+ Console.WriteLine($"Distance Changed : {x}");
+ Value = x;
+ };
+ }
+ }
+}
--- /dev/null
+using MapMyRun.Models.Workout.Items;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Timers;
+
+namespace MapMyRun.Tizen.ViewModels.WorkoutMain
+{
+ class DurationViewModel : BasePageModel, IWorkoutItemViewModel
+ {
+
+ private string _val;
+ private string _valWithUnit;
+ private int _seconds;
+ private Timer secondsTimer;
+
+ public string Value
+ {
+ get { return _val; }
+ set
+ {
+ SetProperty(ref _val, value);
+ ValueWithUnit = value;
+ }
+ }
+
+ public string ValueWithUnit
+ {
+ get { return _valWithUnit; }
+ set
+ {
+ SetProperty(ref _valWithUnit, value + Item.GetUnit());
+ }
+ }
+
+ public string LongLabel
+ {
+ get { return Item.GetLabel(WorkoutItemLabelType.LongLabel); }
+ }
+
+ public string MediumLabel
+ {
+ get { return Item.GetLabel(WorkoutItemLabelType.MediumLabel); }
+ }
+
+ public string ShortLabel
+ {
+ get { return Item.GetLabel(WorkoutItemLabelType.ShortLabel); }
+ }
+
+ public WorkoutItemType ItemType { get; set; }
+ public IWorkoutItem Item { get; set; }
+
+ public DurationViewModel(IWorkoutItem item)
+ {
+ ItemType = WorkoutItemType.Duration;
+ Item = item;
+ Value = item.GetEmptyValue();
+ }
+
+ public int Seconds
+ {
+ get { return _seconds; }
+ set
+ {
+ _seconds = value;
+ int h = _seconds / 3600;
+ int m = (_seconds - h * 3600) / 60;
+ int s = _seconds - h * 3600 - m * 60;
+ Value = (h == 0 ? "" : $"{h}:") + (h > 0 && m < 10 ? "0" : "") + $"{m}:" + (s < 10 ? "0" : "") + $"{s}";
+ }
+ }
+
+ public void Initialize()
+ {
+ Console.WriteLine("Duration Initialize!");
+
+ secondsTimer = new System.Timers.Timer(1000);
+ secondsTimer.Elapsed += (s, e) =>
+ {
+ Seconds++;
+ };
+ secondsTimer.AutoReset = true;
+ secondsTimer.Enabled = true;
+ }
+
+ public void StopTimer()
+ {
+ secondsTimer.Enabled = false;
+ }
+
+ public void ResumeTimer()
+ {
+ secondsTimer.Enabled = true;
+ }
+ }
+}
--- /dev/null
+using MapMyRun.Models.Workout.Items;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace MapMyRun.Tizen.ViewModels.WorkoutMain
+{
+ public interface IWorkoutItemViewModel
+ {
+ string Value { get; set; }
+ string ValueWithUnit { get; }
+ string LongLabel { get; }
+ string MediumLabel { get; }
+ string ShortLabel { get; }
+ WorkoutItemType ItemType { get; set; }
+ IWorkoutItem Item { get; set; }
+ void Initialize();
+ }
+}
--- /dev/null
+using MapMyRun.Models.Workout.Items;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Timers;
+
+namespace MapMyRun.Tizen.ViewModels.WorkoutMain
+{
+ class PeakHrViewModel : BasePageModel, IWorkoutItemViewModel
+ {
+ private string _val;
+ private string _valWithUnit;
+ public string Value
+ {
+ get { return _val; }
+ set
+ {
+ string nextValue = String.Equals(value, "0", StringComparison.OrdinalIgnoreCase) ? Item.GetEmptyValue() : value;
+
+ SetProperty(ref _val, nextValue);
+ ValueWithUnit = nextValue;
+ }
+ }
+
+ public string ValueWithUnit
+ {
+ get { return _valWithUnit; }
+ set
+ {
+ SetProperty(ref _valWithUnit, value + Item.GetUnit());
+ }
+ }
+
+ public string LongLabel
+ {
+ get { return Item.GetLabel(WorkoutItemLabelType.LongLabel); }
+ }
+
+ public string MediumLabel
+ {
+ get { return Item.GetLabel(WorkoutItemLabelType.MediumLabel); }
+ }
+
+ public string ShortLabel
+ {
+ get { return Item.GetLabel(WorkoutItemLabelType.ShortLabel); }
+ }
+
+ public WorkoutItemType ItemType { get; set; }
+ public IWorkoutItem Item { get; set; }
+
+ public PeakHrViewModel(IWorkoutItem item)
+ {
+ ItemType = WorkoutItemType.PeakHR;
+ Item = item;
+ Value = item.GetEmptyValue();
+ }
+
+ public void Initialize()
+ {
+ Console.WriteLine("PeakHR Initialize!");
+
+ var item = (PeakHr)Item;
+ item.ValueChanged += (x) =>
+ {
+ Console.WriteLine($"PeakHR Changed : {x}");
+ Value = x;
+ };
+ }
+ }
+}
--- /dev/null
+using MapMyRun.Models.Workout.Items;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Timers;
+
+namespace MapMyRun.Tizen.ViewModels.WorkoutMain
+{
+ class PeakPaceViewModel : BasePageModel, IWorkoutItemViewModel
+ {
+ private string _val;
+ private string _valWithUnit;
+ public string Value
+ {
+ get { return _val; }
+ set
+ {
+ SetProperty(ref _val, value);
+ ValueWithUnit = value;
+ }
+ }
+
+ public string ValueWithUnit
+ {
+ get { return _valWithUnit; }
+ set
+ {
+ SetProperty(ref _valWithUnit, value + Item.GetUnit());
+ }
+ }
+
+ public string LongLabel
+ {
+ get { return Item.GetLabel(WorkoutItemLabelType.LongLabel); }
+ }
+
+ public string MediumLabel
+ {
+ get { return Item.GetLabel(WorkoutItemLabelType.MediumLabel); }
+ }
+
+ public string ShortLabel
+ {
+ get { return Item.GetLabel(WorkoutItemLabelType.ShortLabel); }
+ }
+
+ public WorkoutItemType ItemType { get; set; }
+ public IWorkoutItem Item { get; set; }
+
+ public PeakPaceViewModel(IWorkoutItem item)
+ {
+ ItemType = WorkoutItemType.MaxPace;
+ Item = item;
+ Value = item.GetEmptyValue();
+ }
+
+ public void Initialize()
+ {
+ Console.WriteLine("MaxPace Initialize!");
+
+ var item = (MaxPace)Item;
+ item.ValueChanged += (x) =>
+ {
+ Console.WriteLine($"MaxPace Changed : {x}");
+ Value = x;
+ };
+ }
+ }
+}
--- /dev/null
+using MapMyRun.Models.Workout;
+using MapMyRun.Models.Workout.Items;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace MapMyRun.Tizen.ViewModels.WorkoutMain
+{
+ class WorkoutMainPageModel : BasePageModel
+ {
+ private readonly Workout _workout;
+ Dictionary<int, IWorkoutItemViewModel> _workoutItemViewModels = new Dictionary<int, IWorkoutItemViewModel>();
+
+ public WorkoutMainPageModel(Workout workout)
+ {
+ _workout = workout;
+ }
+
+ public IWorkoutItemViewModel GetWorkoutViewModel(int page, int number)
+ {
+ //TODO : Model should provide valid workitem list per each activity and check here.
+ WorkoutItemType[,] workoutItemSet = new WorkoutItemType[,] {
+ {WorkoutItemType.Duration, WorkoutItemType.Distance, WorkoutItemType.Calories, WorkoutItemType.CurrentHR },
+ {WorkoutItemType.Pace, WorkoutItemType.MaxPace, WorkoutItemType.Cadence, WorkoutItemType.CurrentHR},
+ {WorkoutItemType.PeakHR, WorkoutItemType.AvgHR, WorkoutItemType.CurrentHR, WorkoutItemType.HRZone }
+ };
+
+ if (page > workoutItemSet.GetUpperBound(0))
+ return null;
+
+ if (number > workoutItemSet.GetUpperBound(1))
+ return null;
+
+ var type = workoutItemSet[page, number];
+ var itemViewModel = GetItemViewModel(type);
+
+ return itemViewModel;
+ }
+
+ IWorkoutItemViewModel GetItemViewModel(WorkoutItemType type)
+ {
+ if (_workoutItemViewModels.TryGetValue((int)type, out IWorkoutItemViewModel item))
+ return item;
+
+ var itemViewModel = CreateViewModel(_workout.GetItem(type));
+ if (itemViewModel == null)
+ return null;
+
+ itemViewModel.Initialize();
+ _workoutItemViewModels.Add((int)type, itemViewModel);
+ return itemViewModel;
+ }
+
+ IWorkoutItemViewModel CreateViewModel(IWorkoutItem item)
+ {
+ IWorkoutItemViewModel ret = null;
+
+ switch (item.GetWorkoutItemType())
+ {
+ case WorkoutItemType.Duration:
+ ret = new DurationViewModel(item);
+ break;
+ case WorkoutItemType.Calories:
+ ret = new CaloriesViewModel(item);
+ break;
+ case WorkoutItemType.Distance:
+ ret = new DistanceViewModel(item);
+ break;
+ case WorkoutItemType.CurrentHR:
+ ret = new CurrentHrViewModel(item);
+ break;
+ case WorkoutItemType.PeakHR:
+ ret = new PeakHrViewModel(item);
+ break;
+ case WorkoutItemType.AvgHR:
+ ret = new AverageHrViewModel(item);
+ break;
+ case WorkoutItemType.HRZone:
+ ret = new ZoneHrViewModel(item);
+ break;
+ case WorkoutItemType.Pace:
+ ret = new CurrentPaceViewModel(item);
+ break;
+ case WorkoutItemType.MaxPace:
+ ret = new PeakPaceViewModel(item);
+ break;
+ case WorkoutItemType.Cadence:
+ ret = new CadenceViewModel(item);
+ break;
+ default:
+ break;
+ }
+ return ret;
+ }
+ }
+}
--- /dev/null
+using MapMyRun.Models.Workout.Items;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Timers;
+
+namespace MapMyRun.Tizen.ViewModels.WorkoutMain
+{
+ class ZoneHrViewModel : BasePageModel, IWorkoutItemViewModel
+ {
+ private string _val;
+ private string _valWithUnit;
+ public string Value
+ {
+ get { return _val; }
+ set
+ {
+ SetProperty(ref _val, value);
+ ValueWithUnit = value;
+ }
+ }
+
+ public string ValueWithUnit
+ {
+ get { return _valWithUnit; }
+ set
+ {
+ SetProperty(ref _valWithUnit, value + Item.GetUnit());
+ }
+ }
+
+ public string LongLabel
+ {
+ get { return Item.GetLabel(WorkoutItemLabelType.LongLabel); }
+ }
+
+ public string MediumLabel
+ {
+ get { return Item.GetLabel(WorkoutItemLabelType.MediumLabel); }
+ }
+
+ public string ShortLabel
+ {
+ get { return Item.GetLabel(WorkoutItemLabelType.ShortLabel); }
+ }
+
+ public WorkoutItemType ItemType { get; set; }
+ public IWorkoutItem Item { get; set; }
+
+ public ZoneHrViewModel(IWorkoutItem item)
+ {
+ ItemType = WorkoutItemType.HRZone;
+ Item = item;
+ Value = item.GetEmptyValue();
+ }
+
+ public void Initialize()
+ {
+ Console.WriteLine("ZoneHr Initialize!");
+
+ var item = (ZoneHr)Item;
+ item.ValueChanged += (x) =>
+ {
+ Console.WriteLine($"ZoneHr Changed : {x}");
+ Value = x;
+ };
+ }
+ }
+}
--- /dev/null
+using MapMyRun.Models;
+using MapMyRun.Tizen.Models;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using Tizen.Applications;
+
+namespace MapMyRun.Tizen.ViewModels
+{
+ public class WorkoutSetupPageModel : BasePageModel
+ {
+ private ActivityRepository activityRepository;
+
+ public List<Activity> defaultList { get; set; }
+
+ private string _currentActivity;
+ public string currentActivity {
+ get
+ {
+ return _currentActivity;
+ }
+
+ set
+ {
+ SetProperty(ref _currentActivity, value, "currentActivity");
+ }
+ }
+
+ public WorkoutSetupPageModel()
+ {
+ string fileLocation = Application.Current.DirectoryInfo.Resource + $"localization/en.json";
+ string json;
+
+ using (StreamReader reader = new StreamReader(fileLocation))
+ {
+ json = reader.ReadToEnd();
+ }
+
+ activityRepository = new ActivityRepository(json);
+
+ GetPopularList();
+
+ currentActivity = activityRepository.popularActivityList.Find((Activity activity) =>
+ {
+ return activity.id == 16; // Default set to Run for now
+ })?.name;
+ }
+
+ public void GetPopularList()
+ {
+ defaultList = activityRepository.popularActivityList;
+ defaultList.Add(new Activity() { name = "All Activities", group_id = -1 });
+ }
+
+ public List<Activity> GetGroupActivities()
+ {
+ return activityRepository.activityGroupings;
+ }
+
+ public List<Activity> GetActivitiesByGroup(int group_id)
+ {
+ return activityRepository.GetActivitiesByGroupId(group_id);
+ }
+
+ public void updateCurrentActivity(Activity activity)
+ {
+ currentActivity = activity.name;
+ }
+ }
+}
--- /dev/null
+{
+ "GPS": "GPS",
+ "start_workout": "START WORKOUT",
+ "my_workouts": "My Workouts",
+ "all_activities_title": "All Activities",
+ "all_activities_list_item": "All Activities",
+ "activity_history_name_on": "on",
+ "select_exercise_menu_item": "Select Activity",
+ "my_challenges": "My Challenges",
+ "select_exercise_list_title": "Select Activity",
+ "workout_finish": "Finish",
+ "mi_header": "mi",
+ "m_per_s": "(m/s)",
+ "workout_save": "Save",
+ "workout_discard": "Discard",
+ "workout_discard_confirmation": "Are you sure you\nwant to discard\nthis workout?\n\nAll data will\nbe lost.",
+ "workout_cancel_discard": "Cancel",
+ "workout_pause": "Pause",
+ "workout_resume": "Resume",
+ "workout_with_another_app": "A workout is already active in another app.",
+ "workout_save_confirmation_title": "Your workout has been saved.",
+ "workout_save_confirmation_subtitle": "Allow a few minutes for workout to sync across all devices.",
+ "workout_summary": "Workout\nSummary",
+ "workout_no_network_connection_title": "No network\nconnection.",
+ "workout_no_network_connection_subtitle": "Workout will sync when connection is restored.",
+ "login_request": "Please log in to MapMyRun on your phone.",
+ "login": "LOG IN",
+ "install_request": "Please install MapMyRun on your phone.",
+ "install": "INSTALL",
+ "network_request": "No Network Connection",
+ "bluetooth_request": "No Bluetooth Connection",
+ "try_again": "TRY AGAIN",
+ "km_header": "km",
+ "button_ok": "OK",
+ "profile_data_description": "We need a few more stats from you to calculate calorie burn.",
+ "profile_data_description_standalone": "To estimate\ncalorie burn,\nplease enter your\nheight and weight\nin the MapMyRun\napp on your\nphone.",
+ "profile_data_complete_on_phone": "COMPLETE ON PHONE",
+ "profile_data_skip": "SKIP",
+ "profile_data_redirected_description": "It may take several minutes for Gear to reflect these changes.",
+ "profile_data_skipped_description": "To edit these stats later, open MapMyRun on your phone and go to Edit Profile.",
+ "workout_didnt_sync": "Workout didn't sync. No network connection.",
+ "workout_has_synced": "Your MapMyRun workout has synced.",
+ "workout_hrs": "hrs",
+ "workout_min": "min",
+ "workout_min_per_mi": "min/mi",
+ "workout_min_per_km": "min/km",
+ "workout_mi": "mi",
+ "workout_km": "km",
+ "workout_mi_per_hr": "mph",
+ "workout_km_per_hr": "km/h",
+ "workout_save_or_discard": "Save or Discard this workout.",
+ "workout_paused": "Paused",
+ "settings": "Settings",
+ "feedback": "Feedback",
+ "haptic_feedback": "Vibration",
+ "voice_feedback": "Voice",
+ "voice_feedback_on": "On",
+ "voice_feedback_off": "Off",
+ "voice_feedback_distance": "Distance",
+ "voice_feedback_duration": "Duration",
+ "voice_feedback_avg_pace_speed": "Avg. Pace/Speed",
+ "voice_feedback_current_pace_speed": "Current Pace/Speed",
+ "voice_feedback_split_pace_speed": "Split Pace/Speed",
+ "voice_feedback_cadence": "Cadence",
+ "voice_feedback_stride_length": "Stride Length",
+ "voice_feedback_current_hr": "Current HR",
+ "settings_min": "min",
+ "settings_mi": "mi",
+ "settings_km": "km",
+ "settings_interval": "Interval",
+ "done": "done",
+ "gps_off_notification": "Maps unavailable.\nEnsure GPS is\nactive on your\nphone and\nyour Gear.",
+ "map_acquiring_gps": "Attempting\nto acquire\nGPS signal.",
+ "pairing_error": "To complete\npairing, tap\n¡°Accept¡± on your phone.",
+ "update_fw": "Please update\nyour Gear\nfirmware then\nre-install the app.",
+ "wko_resume_error": "Unable to resume\nworkout.\n\nYour workout\nhas been saved.",
+ "awards": "Awards",
+ "top": "Top #%",
+ "zero_day_left": "# days left",
+ "one_day_left": "# day left",
+ "two_day_left": "# days left",
+ "few_day_left": "# days left",
+ "many_day_left": "# days left",
+ "other_day_left": "# days left",
+ "network_offline": "Network offline.\n\nTry again when\nnetwork connection\nis restored.",
+ "challenges_list_empty": "No challenges\nyet.\n\nUse your\nphone to find\na challenge.",
+ "map_unable_to_acquire_gps": "Unable\nto acquire\nGPS signal.\n\nPlease move\nto open space.",
+ "rate_app_question": "Do you like using\nMapMyRun for\nSamsung Gear?",
+ "post_review_question": "Great! Would you\ntake a moment\nto post a review?",
+ "yes": "YES",
+ "no": "NO",
+ "no_workout_stats": "This workout\ndoes not have\nany stats.",
+ "workouts_list_empty": "No workouts yet.",
+ "this_week": "This week",
+ "last_week": "Last week",
+ "last_thirty_days": "Last 30 days",
+ "older": "Older",
+ "group_class": "Class",
+ "group_cycling": "Cycling",
+ "group_dance": "Dance",
+ "group_gym": "Gym",
+ "group_martial_arts": "Martial Arts",
+ "group_other": "Other",
+ "group_run": "Run",
+ "group_sport": "Sport",
+ "group_swim": "Swim",
+ "group_walk": "Walk",
+ "group_winter": "Winter",
+ "group_yoga": "Yoga",
+ "activity_id_1": "Generic Workout",
+ "activity_id_7": "Fartleks",
+ "activity_id_9": "Walk",
+ "activity_id_10": "Winter Sport / Activity",
+ "activity_id_16": "Run",
+ "activity_id_19": "Indoor Bike Ride",
+ "activity_id_20": "Swim",
+ "activity_id_21": "Other Sport",
+ "activity_id_22": "Rock Climbing",
+ "activity_id_23": "Class Workout",
+ "activity_id_24": "Hike",
+ "activity_id_26": "Total Body",
+ "activity_id_28": "Circuit Training",
+ "activity_id_29": "Sailing",
+ "activity_id_31": "Pilates",
+ "activity_id_33": "Fixed Gear (Fixie)",
+ "activity_id_34": "Aerobics",
+ "activity_id_36": "Road Cycling",
+ "activity_id_41": "Mountain Biking",
+ "activity_id_43": "Yard Work",
+ "activity_id_44": "Track",
+ "activity_id_45": "Triathlon",
+ "activity_id_47": "Unicycling",
+ "activity_id_54": "Volleyball",
+ "activity_id_56": "BMX",
+ "activity_id_57": "Canoeing",
+ "activity_id_59": "Class / Aerobics",
+ "activity_id_60": "CycloCross",
+ "activity_id_65": "Boxing",
+ "activity_id_68": "Softball",
+ "activity_id_71": "Brick",
+ "activity_id_72": "Other Martial Arts",
+ "activity_id_73": "Tennis",
+ "activity_id_74": "Snow Skiing",
+ "activity_id_75": "Lap",
+ "activity_id_77": "House Work",
+ "activity_id_80": "Snorkeling",
+ "activity_id_82": "Ballet",
+ "activity_id_84": "Wakeboarding",
+ "activity_id_85": "Fishing",
+ "activity_id_86": "Ice Skating",
+ "activity_id_94": "Wind Surfing",
+ "activity_id_95": "Skateboarding",
+ "activity_id_96": "Nordic Track",
+ "activity_id_99": "Rowing Machine",
+ "activity_id_101": "Roller Skating",
+ "activity_id_102": "Commute",
+ "activity_id_103": "Sprints",
+ "activity_id_104": "Curling",
+ "activity_id_105": "Power",
+ "activity_id_107": "Snowboarding",
+ "activity_id_108": "Track",
+ "activity_id_111": "Water",
+ "activity_id_115": "Time Trial",
+ "activity_id_116": "Bowling",
+ "activity_id_119": "Snowshoeing",
+ "activity_id_120": "Spin Class",
+ "activity_id_121": "Water Polo",
+ "activity_id_123": "Bootcamp",
+ "activity_id_124": "Group",
+ "activity_id_127": "Surfing",
+ "activity_id_128": "Rowing",
+ "activity_id_131": "Wrestling",
+ "activity_id_133": "Stairs",
+ "activity_id_134": "Baseball",
+ "activity_id_135": "Hockey",
+ "activity_id_137": "Horseback Riding",
+ "activity_id_138": "Whitewater Rafting",
+ "activity_id_141": "Ice Hockey",
+ "activity_id_142": "Badminton",
+ "activity_id_148": "Cheerleading",
+ "activity_id_153": "Ultimate",
+ "activity_id_154": "Golf",
+ "activity_id_155": "Fencing",
+ "activity_id_156": "Water Skiing",
+ "activity_id_161": "Field Hockey",
+ "activity_id_163": "Squash",
+ "activity_id_164": "Yolates",
+ "activity_id_169": "Inline Skating",
+ "activity_id_171": "Skydiving",
+ "activity_id_172": "Race/Event",
+ "activity_id_176": "Football / Soccer",
+ "activity_id_178": "Lacrosse",
+ "activity_id_180": "Open Water",
+ "activity_id_182": "Rollerskiing",
+ "activity_id_184": "Scuba Diving",
+ "activity_id_192": "Water Aerobics",
+ "activity_id_193": "Group",
+ "activity_id_197": "Intervals",
+ "activity_id_199": "Calisthenics",
+ "activity_id_200": "Gardening",
+ "activity_id_201": "Adventure Race / Event",
+ "activity_id_204": "Dog Walk",
+ "activity_id_205": "Kitesurfing",
+ "activity_id_208": "Treadmill",
+ "activity_id_214": "Physical Therapy",
+ "activity_id_216": "Soccer, Indoor",
+ "activity_id_221": "Bouldering",
+ "activity_id_222": "Floorball",
+ "activity_id_224": "Farming",
+ "activity_id_227": "Hill Workout",
+ "activity_id_228": "Racquetball",
+ "activity_id_230": "Workout Video",
+ "activity_id_235": "Rugby",
+ "activity_id_250": "Elliptical",
+ "activity_id_251": "Kickball",
+ "activity_id_254": "Gymnastics",
+ "activity_id_257": "Kayak",
+ "activity_id_258": "Netball",
+ "activity_id_259": "Basketball",
+ "activity_id_263": "Jump Rope",
+ "activity_id_264": "Hunting",
+ "activity_id_266": "Trail",
+ "activity_id_267": "Wood Chopping",
+ "activity_id_271": "Cricket",
+ "activity_id_275": "Nordic, Walk",
+ "activity_id_279": "Indoor",
+ "activity_id_284": "Diving, Board",
+ "activity_id_336": "Beach Volleyball",
+ "activity_id_468": "Tai Chi",
+ "activity_id_469": "Judo",
+ "activity_id_471": "Karate",
+ "activity_id_472": "Kickboxing",
+ "activity_id_473": "Aikido",
+ "activity_id_498": "Bikram",
+ "activity_id_499": "Vinyasa",
+ "activity_id_500": "Yoga",
+ "activity_id_546": "Indoor Trainer",
+ "activity_id_548": "Stationary Bike",
+ "activity_id_564": "Sparring",
+ "activity_id_598": "Upper Body",
+ "activity_id_622": "Dance Class",
+ "activity_id_627": "Step Aerobics Class",
+ "activity_id_633": "Race",
+ "activity_id_637": "Commute",
+ "activity_id_704": "CrossFit Class",
+ "activity_id_708": "Motocross",
+ "activity_id_710": "ElliptiGo",
+ "activity_id_714": "Longboarding",
+ "activity_id_722": "Hot",
+ "activity_id_724": "Power",
+ "activity_id_726": "Ashtanga",
+ "activity_id_728": "Ergometer",
+ "activity_id_730": "Stair Machine",
+ "activity_id_732": "Basic Training",
+ "activity_id_740": "Tae Bo",
+ "activity_id_742": "Ballroom",
+ "activity_id_746": "Hip Hop",
+ "activity_id_748": "Hula",
+ "activity_id_750": "Latin",
+ "activity_id_752": "Salsa",
+ "activity_id_754": "Zumba",
+ "activity_id_756": "Indoor Track",
+ "activity_id_758": "Handball",
+ "activity_id_760": "Jujitsu",
+ "activity_id_762": "Kung Fu",
+ "activity_id_764": "Tae Kwon Do",
+ "activity_id_768": "American Football",
+ "activity_id_813": "Mixed Martial Arts",
+ "activity_id_819": "Table Tennis",
+ "activity_id_825": "Stroller - Single",
+ "activity_id_827": "Stroller - Double",
+ "activity_id_829": "Stroller - Single",
+ "activity_id_831": "Stroller - Double",
+ "activity_id_845": "Trampoline",
+ "activity_id_855": "Dog Run",
+ "activity_id_861": "Jacobs Ladder",
+ "activity_id_863": "Stand Up Paddling",
+ "activity_id_880": "Muay Thai",
+ "activity_id_882": "Barre Workout",
+ "activity_id_890": "Legs",
+ "select_stat": "Select Stat",
+ "label_distance": "Distance",
+ "label_pace": "Pace, Current",
+ "label_pace_avg": "Pace, Average",
+ "label_pace_max": "Pace, Max",
+ "label_speed": "Speed, Current",
+ "label_speed_avg": "Speed, Average",
+ "label_speed_max": "Speed, Max",
+ "label_calories": "Calories",
+ "label_hr": "Heart Rate, Current",
+ "label_hr_avg": "Heart Rate, Average",
+ "label_hr_max": "Heart Rate, Max",
+ "label_intensity": "Intensity",
+ "label_willpower": "WILLPOWER¢â",
+ "label_duration": "Duration",
+ "label_distance_medium": "DISTANCE",
+ "label_pace_medium": "PACE",
+ "label_pace_avg_medium": "AVG PACE",
+ "label_pace_max_medium": "MAX PACE",
+ "label_speed_medium": "SPEED",
+ "label_speed_avg_medium": "AVG SPEED",
+ "label_speed_max_medium": "MAX SPEED",
+ "label_calories_medium": "CALORIES",
+ "label_hr_medium": "HR",
+ "label_hr_avg_medium": "AVG HR",
+ "label_hr_max_medium": "MAX HR",
+ "label_intensity_medium": "INTENSITY",
+ "label_willpower_medium": "WILLPOWER",
+ "label_duration_medium": "DURATION",
+ "label_hr_zone_medium": "ZONE",
+ "label_distance_short": "DIST",
+ "label_pace_short": "PACE",
+ "label_pace_avg_short": "AVG PACE",
+ "label_pace_max_short": "MAX PACE",
+ "label_speed_short": "SPEED",
+ "label_speed_avg_short": "AVG SPEED",
+ "label_speed_max_short": "MAX SPEED",
+ "label_calories_short": "CAL",
+ "label_hr_short": "HR",
+ "label_hr_avg_short": "AVG HR",
+ "label_hr_max_short": "MAX HR",
+ "label_intensity_short": "INT",
+ "label_willpower_short": "WILLPWR",
+ "label_duration_short": "DUR",
+ "cal": "cal",
+ "bpm": "bpm",
+ "stat_ux_tip": "Tap on screen to rotate stats.\nLong-press on a stat to swap.",
+ "map_ux_tip": "Tap on screen to show stats.",
+ "consent_error": "Please open\nMapMyRun on\nyour phone to\naccept updated\nterms and privacy\npolicy.",
+ "consent_waiting": "Open MapMyRun\non your phone to\ncontinue.",
+ "workout_steps_per_min": "spm",
+ "workout_meter": "m",
+ "workout_inch": "in",
+ "workout_centimeter": "cm",
+ "label_cadence": "Cadence, Current",
+ "label_cadence_avg": "Cadence, Average",
+ "label_stride_length": "Stride length, Current",
+ "label_stride_length_avg": "Stride length, Average",
+ "label_steps": "Total Steps",
+ "label_cadence_medium": "CADENCE",
+ "label_cadence_avg_medium": "AVG CADENCE",
+ "label_stride_length_medium": "STRIDE",
+ "label_stride_length_avg_medium": "AVG STRIDE",
+ "label_steps_medium": "STEPS",
+ "label_cadence_short": "CAD",
+ "label_cadence_avg_short": "AVG CAD",
+ "label_stride_length_short": "STRIDE",
+ "label_stride_length_avg_short": "AVG STRIDE",
+ "label_steps_short": "STEPS",
+ "select": "SELECT",
+ "wko_details_cardio": "Heart Rate",
+ "wko_details_form": "Form",
+ "goal_complete": "GOAL COMPLETE",
+ "target_goal": "Target: {{goal}}",
+ "form_coaching_edu_header_form": "Find Your\nForm",
+ "form_coaching_edu_header_cadence": "Find Your\nCadence",
+ "form_coaching_edu_screen_text_form": "Achieving your optimal\nrunning form can make\nrunning feel easier, better\nmanage your risk of injury,\nand improve your\nperformance.\n\nThe Form Coaching\nfeature will monitor your\nCadence during your run,\nalerting you if you\nfall outside your\n ideal range.Your\nrange varies and is\ncalculated by your\nrunning stats, which can\nchange throughout your run.\n\nIf you fall outside of\nyour target range, try\nincreasing or decreasing\nyour step rate while\nmaintaining the same pace.",
+ "form_coaching_edu_screen_text_cadence": "Understanding your\nrunning form helps\nimprove your overall run.\nOne factor that can\naffect your workout\nis Cadence, measured\nby the total number of\nsteps you take per minute.\nYour target range\nis based on a\nvariety of factors\nincluding age, height,\nweight, gender, and pace.\n\nWe¡¯ll let you know\nwhen you¡¯re below\nor above your target\nrange during your workout.\n\nTo get a deeper\nrunning form breakdown,\nincluding personalized\nform coaching tips,\nview this workout\nin the MapMyRun app\non your mobile phone.",
+ "form_coaching_edu_do_not_show": "Don't show again",
+ "goal_finished": "GOAL!",
+ "workout_setup": "Workout Setup",
+ "goal_item": "Goal",
+ "goal_setup": "What's your\ngoal today?",
+ "goal_basic_workout": "Basic Workout",
+ "goal_distance": "Distance",
+ "goal_duration": "Duration",
+ "goal_pace": "Pace",
+ "split_updates": "Split Updates",
+ "goal_coaching": "Goal Feedback",
+ "form_coaching": "Form Coaching",
+ "split_updates_no_feedback": "No Feedback",
+ "split_updates_audio_frequency": "Frequency",
+ "split_updates_audio": "Audio",
+ "split_updates_audio_desc": "Audio update of\nworkout stats",
+ "split_updates_haptic": "Vibration",
+ "split_updates_haptic_desc": "Vibrate at set frequency",
+ "form_coaching_no_feedback": "No Feedback",
+ "form_coaching_audio": "Audio",
+ "form_coaching_audio_desc": "Receive audio alerts about your running form",
+ "form_coaching_haptic": "Vibration",
+ "form_coaching_haptic_desc": "Vibrate when Cadence is\nout-of-range",
+ "form_coaching_display": "Display",
+ "form_coaching_display_desc": "Auto-transition to the\nForm Coach screen",
+ "goal_coaching_no_feedback": "No Feedback",
+ "goal_coaching_audio": "Audio",
+ "goal_coaching_audio_progress_desc": "Alert at 25% intervals",
+ "goal_coaching_audio_range_desc": "Receive audio guidance when out-of-range",
+ "goal_coaching_haptic": "Vibration",
+ "goal_coaching_haptic_progress_desc": "Vibrate at 25% intervals",
+ "goal_coaching_haptic_range_desc": "Vibrate when out-of-range",
+ "voice_feedback_avg_speed": "Avg. Speed",
+ "voice_feedback_current_speed": "Current Speed",
+ "voice_feedback_split_speed": "Split Speed",
+ "voice_feedback_avg_pace": "Avg. Pace",
+ "voice_feedback_current_pace": "Current Pace",
+ "voice_feedback_split_pace": "Split Pace",
+ "today_workout": "Today's Workout",
+ "coaching_warm_up": "Warm Up",
+ "debug_settings": "Debug Settings",
+ "debug_logging": "Debug Log"
+}
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<manifest package="org.tizen.example.MapMyRun" version="1.0.0" api-version="4" xmlns="http://tizen.org/ns/packages">
+ <profile name="wearable" />
+ <ui-application appid="org.tizen.example.MapMyRun" exec="MapMyRun.Tizen.dll" multiple="false" nodisplay="false" taskmanage="true" type="dotnet" launch_mode="single">
+ <label>MapMyRun</label>
+ <icon>MapMyRun.png</icon>
+ <metadata key="http://tizen.org/metadata/prefer_dotnet_aot" value="true" />
+ <splash-screens>
+ <splash-screen indicator-display="false" orientation="portrait" src="MapMyRun.png" type="img" />
+ </splash-screens>
+ </ui-application>
+ <shortcut-list />
+ <privileges>
+ <privilege>http://tizen.org/privilege/appmanager.launch</privilege>
+ </privileges>
+ <dependencies />
+ <provides-appdefined-privileges />
+</manifest>
\ No newline at end of file
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Tizen.Wearable.CircularUI.Forms;
+using Xamarin.Forms;
+
+namespace MapMyRun.Tizen.Views
+{
+ static class CircleProgressBarSurfaceItemExtension
+ {
+ const string CircleProgressBarAnimationName = "CircleProgressBarProgressTo";
+
+ public static void ProgressTo(this CircleProgressBarSurfaceItem item, double toValue, uint duration)
+ {
+ var tcs = new TaskCompletionSource<bool>();
+ var circlePage = item.Parent as CirclePage;
+ var weakRef = new WeakReference<CircleProgressBarSurfaceItem>(item);
+
+ new Animation(v => {
+ if (weakRef.TryGetTarget(out var pg))
+ {
+ pg.Value = v;
+ }
+ }, item.Value, toValue, Easing.SinOut).Commit(circlePage, CircleProgressBarAnimationName, 16, duration, finished: (f, a) => tcs.SetResult(a));
+ }
+
+ public static void ProgressTo(this CircleProgressBarSurfaceItem item, double fromValue, double toValue, uint duration)
+ {
+ item.Value = fromValue;
+ item.ProgressTo(toValue, duration);
+ }
+
+ public static void CancelAnimation(this CircleProgressBarSurfaceItem item)
+ {
+ var circlePage = item.Parent as CirclePage;
+ circlePage.AbortAnimation(CircleProgressBarAnimationName);
+ }
+ }
+}
--- /dev/null
+<?xml version="1.0" encoding="utf-8" ?>
+<c:CirclePage
+ xmlns="http://xamarin.com/schemas/2014/forms"
+ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+ xmlns:c="clr-namespace:Tizen.Wearable.CircularUI.Forms;assembly=XSF"
+ x:Class="MapMyRun.Tizen.Views.LoadingPage"
+ NavigationPage.HasNavigationBar="False" >
+ <c:CirclePage.Content>
+ <StackLayout
+ HorizontalOptions="Center"
+ Orientation="Vertical"
+ VerticalOptions="Center">
+ <Image
+ Aspect="AspectFill"
+ Source="splash.circle.png" />
+ </StackLayout>
+ </c:CirclePage.Content>
+ <c:CirclePage.CircleSurfaceItems>
+ <c:CircleProgressBarSurfaceItem
+ x:Name="ProgressBar"
+ BarLineWidth="11"
+ BackgroundLineWidth="0"
+ BarColor="#FFFFFF"
+ BackgroundColor="#00FFFFFF"
+ BarRadius="175"
+ IsVisible="True"
+ Value="0"/>
+ </c:CirclePage.CircleSurfaceItems>
+</c:CirclePage>
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Xamarin.Forms;
+using Xamarin.Forms.Xaml;
+using Tizen.Wearable.CircularUI.Forms;
+using MapMyRun.Tizen.ViewModels;
+using MapMyRun.Tizen.Models;
+using MapMyRun.Models;
+
+namespace MapMyRun.Tizen.Views
+{
+ [XamlCompilation(XamlCompilationOptions.Compile)]
+ public partial class LoadingPage : CirclePage
+ {
+ private readonly Loading _loading;
+ public static readonly BindableProperty ProgressProperty =
+ BindableProperty.Create("Progress",
+ typeof(double),
+ typeof(CircleProgressBarSurfaceItem),
+ propertyChanged: (b, o, n) =>
+ ProgressBarProgressChanged((CircleProgressBarSurfaceItem)b, (double)n));
+
+ public LoadingPage(Loading loading)
+ {
+ InitializeComponent();
+ _loading = loading;
+ BindingContext = new LoadingPageModel(_loading);
+ ProgressBar.SetBinding(ProgressProperty, "Progress");
+ }
+
+ private static void ProgressBarProgressChanged(CircleProgressBarSurfaceItem progressBar, double progress)
+ {
+ progressBar.CancelAnimation();
+ progressBar.ProgressTo(progress, 200);
+ if (progress >= 1)
+ {
+ Device.StartTimer(TimeSpan.FromSeconds(0.3), () =>
+ {
+ Application.Current.MainPage = new NavigationPage(new MainPage());
+ return false;
+ });
+ }
+ }
+ }
+}
--- /dev/null
+<?xml version="1.0" encoding="utf-8" ?>
+<c:CirclePage xmlns="http://xamarin.com/schemas/2014/forms"
+ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+ xmlns:c="clr-namespace:Tizen.Wearable.CircularUI.Forms;assembly=XSF"
+ x:Class="MapMyRun.Tizen.Views.MainPage"
+ RotaryFocusObject="{x:Reference scroller}"
+ NavigationPage.HasNavigationBar="False" >
+ <c:CirclePage.Content>
+ <AbsoluteLayout>
+ <Image x:Name="bg" Source="hero-bg.png" HeightRequest="360" WidthRequest="360" Aspect="Fill" AbsoluteLayout.LayoutBounds="0, 0, 1, 1" AbsoluteLayout.LayoutFlags="All" />
+ <BoxView x:Name="listBg" BackgroundColor="Black" AbsoluteLayout.LayoutBounds="0, 0, 1, 1.2" AbsoluteLayout.LayoutFlags="All"/>
+ <c:CircleScrollView x:Name="scroller" AbsoluteLayout.LayoutBounds="0, 0, 1, 1.2" AbsoluteLayout.LayoutFlags="XProportional,SizeProportional">
+ <StackLayout Padding="0, 80, 0, 160" x:Name="fETarget">
+ <StackLayout x:Name="modifyBtn" Padding="0, 10, 0, 0" HeightRequest="100">
+ <Label x:Name="exerType" Text="{Binding currentActivity}" FontAttributes="Bold" FontSize="14" HorizontalOptions="CenterAndExpand" VerticalOptions="StartAndExpand"/>
+ <Label x:Name="exerGoal" Text="Goal: Basic >" FontAttributes="Bold" FontSize="9" HorizontalOptions="CenterAndExpand" VerticalOptions="StartAndExpand"/>
+ </StackLayout>
+ <RelativeLayout HeightRequest="100" x:Name="startBtn">
+ <Image Source="start-btn-empty.png"
+ Aspect="AspectFill"
+ RelativeLayout.XConstraint=
+ "{ConstraintExpression
+ Type=RelativeToParent,
+ Property=Width,
+ Factor=0.20
+ }"
+ RelativeLayout.YConstraint=
+ "{ConstraintExpression
+ Type=RelativeToParent,
+ Property=Height,
+ Factor=0,
+ Constant=-5
+ }"
+ />
+ <Label
+ x:Name="startLabel"
+ Text="START"
+ FontAttributes="Bold"
+ FontSize="8"
+ RelativeLayout.XConstraint=
+ "{ConstraintExpression
+ Type=RelativeToParent,
+ Property=Width,
+ Factor=0.4,
+ Constant=5
+ }"
+ RelativeLayout.YConstraint=
+ "{ConstraintExpression
+ Type=RelativeToParent,
+ Property=Height,
+ Factor=0.2,
+ Constant=-2
+ }"
+ />
+ </RelativeLayout>
+ <Label HeightRequest="110" x:Name="myWorkoutsBtn" Text="My Workouts" VerticalOptions="Start" HorizontalOptions="CenterAndExpand"/>
+ <Label HeightRequest="110" x:Name="settingsBtn" Text="Settings" VerticalOptions="Start" HorizontalOptions="CenterAndExpand"/>
+ </StackLayout>
+ </c:CircleScrollView>
+ <AbsoluteLayout AbsoluteLayout.LayoutBounds="45, 0, 270, 75" AbsoluteLayout.LayoutFlags="None" x:Name="statusTray">
+ <BoxView Color="#0b141a" Opacity="0.6" CornerRadius="20" AbsoluteLayout.LayoutBounds="0, 0, 1, 1" AbsoluteLayout.LayoutFlags="All"/>
+ <Image x:Name="gpsStatus" Source="{Binding GpsStatusImg}" AbsoluteLayout.LayoutBounds="45, 30, AutoSize, AutoSize" AbsoluteLayout.LayoutFlags="None"/>
+ <Image Source="logo--tray.png" AbsoluteLayout.LayoutBounds="113, 15, 40, 40" AbsoluteLayout.LayoutFlags="None"/>
+ <Image Source="icon-shoes--connecting.png" AbsoluteLayout.LayoutBounds="185, 30, AutoSize, AutoSize" AbsoluteLayout.LayoutFlags="None"/>
+ </AbsoluteLayout>
+ </AbsoluteLayout>
+ </c:CirclePage.Content>
+</c:CirclePage>
\ No newline at end of file
--- /dev/null
+using System;
+using Xamarin.Forms;
+using Xamarin.Forms.Xaml;
+using Tizen.Wearable.CircularUI.Forms;
+using MapMyRun.Tizen.ViewModels;
+using System.Timers;
+using MapMyRun.Models;
+using MapMyRun.Models.Workout;
+using MapMyRun.Models.Settings;
+
+namespace MapMyRun.Tizen.Views
+{
+ [XamlCompilation(XamlCompilationOptions.Compile)]
+ public partial class MainPage : CirclePage
+ {
+ private WorkoutSetupPageModel setupPageModel;
+ private Timer gpsAnimation;
+ private MainPageModel mainPageModel;
+
+ private double MIN_SCALE = 0.7;
+ private double MAX_SCALE = 1.0;
+ private bool Initialized = false;
+
+ public MainPage()
+ {
+ mainPageModel = new MainPageModel();
+ setupPageModel = new WorkoutSetupPageModel();
+
+ InitializeComponent();
+
+ modifyBtn.BindingContext = setupPageModel;
+ statusTray.BindingContext = mainPageModel;
+
+ scroller.Scrolled += OnScrolled;
+ scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Never;
+ AbsoluteLayout.SetLayoutFlags(listBg, AbsoluteLayoutFlags.SizeProportional);
+
+ Appearing += (s, e) =>
+ {
+ // Only initialize black background on first draw
+ if(!Initialized)
+ {
+ Initialized = true;
+ Moved(0);
+ }
+ ToggleGpsAnimation(true);
+ };
+
+ Disappearing += (s, e) =>
+ {
+ ToggleGpsAnimation(false);
+ };
+
+ TapGestureRecognizer modifyTapGesture = new TapGestureRecognizer();
+ modifyTapGesture.Tapped += OnModifyTapped;
+ modifyBtn.GestureRecognizers.Add(modifyTapGesture);
+
+ TapGestureRecognizer startTapGesture = new TapGestureRecognizer();
+ startTapGesture.Tapped += OnStartTapped;
+ startBtn.GestureRecognizers.Add(startTapGesture);
+
+ TapGestureRecognizer historyTapRecognizer = new TapGestureRecognizer();
+ historyTapRecognizer.Tapped += OnHistoryTapped;
+ myWorkoutsBtn.GestureRecognizers.Add(historyTapRecognizer);
+
+ TapGestureRecognizer settingsTapRecognizer = new TapGestureRecognizer();
+ settingsTapRecognizer.Tapped += OnSettingsTapped;
+ settingsBtn.GestureRecognizers.Add(settingsTapRecognizer);
+ }
+
+ async void OnModifyTapped(object sender, EventArgs args)
+ {
+ await Navigation.PushAsync(new WorkoutSetupPage(setupPageModel));
+ }
+
+ async void OnStartTapped(object sender, EventArgs args)
+ {
+ // TODO : Move proper position
+ Workout workout = (Application.Current as App).workout;
+ workout.PrepareWorkout(16, true, true, false);
+ await Navigation.PushAsync(new WorkoutMainLoadingPage());
+ }
+
+ async void OnHistoryTapped(object sender, EventArgs args)
+ {
+ await Navigation.PushAsync(new WorkoutHistoryPage());
+ }
+
+ async void OnSettingsTapped(object sender, EventArgs args)
+ {
+ await Navigation.PushAsync(new SettingsPage());
+ }
+
+ private void ToggleGpsAnimation(bool enabled)
+ {
+ if (enabled)
+ {
+ gpsAnimation = new Timer(1000);
+ gpsAnimation.AutoReset = true;
+ int nextGpsState = 1;
+ gpsAnimation.Elapsed += (se, ev) =>
+ {
+ if (mainPageModel.GpsState == GPSState.Acquiring)
+ {
+ gpsStatus.FadeTo(nextGpsState, 1000);
+ nextGpsState = nextGpsState == 1 ? 0 : 1;
+ } else
+ {
+ gpsStatus.Opacity = 1;
+ }
+ };
+
+ gpsAnimation.Start();
+ }
+ else
+ {
+ gpsAnimation?.Stop();
+ gpsStatus.Opacity = 1;
+ }
+ }
+
+ void OnScrolled(object sender, ScrolledEventArgs e) => Moved(e.ScrollY);
+
+ protected override bool OnBackButtonPressed()
+ {
+ var request = new Request(OperationType.ExitApp, "");
+ var dispatcher = MapMyRun.Models.Dispatcher.Instance;
+
+ request.ResponseReceived += (Response response) =>
+ {
+ Console.WriteLine($"Receive Exit App Signal Response");
+ };
+ dispatcher.SendRequest(request);
+ Console.WriteLine("Sent Exit App Signal");
+
+ return false;
+ }
+
+ void Moved(double y)
+ {
+ double SCREEN_SCROLLER_OFFSET = 16;
+ AbsoluteLayout.SetLayoutBounds(listBg, new Rectangle(0, scroller.Y + startBtn.Y + startBtn.Height / 2 - y - SCREEN_SCROLLER_OFFSET, 1.0, 2));
+ for (int i = 1; i < fETarget.Children.Count; i++)
+ {
+ var view = fETarget.Children[i];
+
+ double distanceFromCenter = y;
+ switch (i)
+ {
+ case 1: // Start button
+ distanceFromCenter = y;
+ break;
+ case 2: // My Workouts
+ distanceFromCenter = Math.Abs(y - 150);
+ break;
+ case 3: // Settings
+ distanceFromCenter = Math.Abs(y - 250);
+ break;
+ }
+ double scale = MAX_SCALE - (distanceFromCenter / 750);
+
+ view.Scale = scale < MIN_SCALE ? MIN_SCALE
+ : scale > MAX_SCALE ? MAX_SCALE
+ : scale;
+ }
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8" ?>
+<c:CirclePage
+ xmlns="http://xamarin.com/schemas/2014/forms"
+ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+ xmlns:c="clr-namespace:Tizen.Wearable.CircularUI.Forms;assembly=XSF"
+ x:Class="MapMyRun.Tizen.Views.SelectWorkoutPage"
+ NavigationPage.HasNavigationBar="False"
+ RotaryFocusObject="{x:Reference scroller}">
+ <c:CirclePage.Content>
+ <AbsoluteLayout>
+ <c:CircleScrollView x:Name="scroller" AbsoluteLayout.LayoutFlags="All" AbsoluteLayout.LayoutBounds="0, 0, 1, 1">
+ <StackLayout>
+ <Label
+ HeightRequest="65"
+ Margin="0, 0, 0, 0"
+ Text="Select"
+ Style="{StaticResource PrimaryHeaderStyle}"/>
+ <Label
+ HeightRequest="65"
+ Margin="0, -30, 0, 50"
+ Text="Activity"
+ Style="{StaticResource PrimaryHeaderStyle}"/>
+ <StackLayout x:Name="activityStack">
+ </StackLayout>
+ </StackLayout>
+ </c:CircleScrollView>
+ <AbsoluteLayout x:Name="selectBtn" IsVisible="False" AbsoluteLayout.LayoutFlags="None" AbsoluteLayout.LayoutBounds="75, 270, 200, 100">
+ <Image Source="select-btn-empty.png" Aspect="AspectFill" Opacity="0.9" />
+ <Label Text="SELECT" FontAttributes="Bold" FontSize="9" AbsoluteLayout.LayoutFlags="SizeProportional" AbsoluteLayout.LayoutBounds="60, 20, 1, 1"/>
+ </AbsoluteLayout>
+ </AbsoluteLayout>
+ </c:CirclePage.Content>
+</c:CirclePage>
--- /dev/null
+using MapMyRun.Models;
+using MapMyRun.Tizen.Models;
+using MapMyRun.Tizen.ViewModels;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Tizen.Wearable.CircularUI.Forms;
+using Xamarin.Forms;
+using Xamarin.Forms.Xaml;
+
+namespace MapMyRun.Tizen.Views
+{
+ [XamlCompilation(XamlCompilationOptions.Compile)]
+ public partial class SelectWorkoutPage : CirclePage
+ {
+ private double MIN_SCALE = 0.6;
+ private double MAX_SCALE = 1.1;
+ private int SCROLL_INTERVAL = 144;
+
+ private WorkoutSetupPageModel setupPageModel;
+ private bool Initialized = false;
+ private double lastScrollPos = 0;
+ private int focusedIndex = 0;
+ private bool isTimerRunning = false;
+ private bool isScrolling = false;
+
+ private void Initialize(WorkoutSetupPageModel _setupPageModel)
+ {
+ InitializeComponent();
+
+ setupPageModel = _setupPageModel;
+ BindingContext = setupPageModel;
+ scroller.Scrolled += OnScrolled;
+ scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Never;
+
+ Appearing += (s, e) =>
+ {
+ if (!Initialized)
+ {
+ Initialized = true;
+ Scroller(0);
+ }
+ };
+
+ TapGestureRecognizer selectTapGesture = new TapGestureRecognizer();
+ selectTapGesture.Tapped += OnSelectTapped;
+ selectBtn.GestureRecognizers.Add(selectTapGesture);
+ }
+
+ public SelectWorkoutPage(WorkoutSetupPageModel _setupPageModel)
+ {
+ Initialize(_setupPageModel);
+ UpdateActivityList(setupPageModel.defaultList);
+ }
+
+ public SelectWorkoutPage(WorkoutSetupPageModel _setupPageModel, int group_id)
+ {
+ Initialize(_setupPageModel);
+
+ if (group_id == -1) // Others
+ {
+ UpdateActivityList(setupPageModel.GetGroupActivities());
+ }
+ else
+ {
+ UpdateActivityList(setupPageModel.GetActivitiesByGroup(group_id));
+ }
+ }
+
+ private Label LabelFactory(Activity activity)
+ {
+
+ ActivityLabel label = new ActivityLabel(activity);
+ label.Text = activity.name;
+ label.HeightRequest = 137;
+ label.VerticalOptions = LayoutOptions.Start;
+ label.HorizontalOptions = LayoutOptions.CenterAndExpand;
+
+ TapGestureRecognizer activityTapGesture = new TapGestureRecognizer();
+ activityTapGesture.Tapped += Activity_Selected;
+ label.GestureRecognizers.Add(activityTapGesture);
+
+ return label;
+ }
+
+ private void UpdateActivityList(List<Activity> activityList)
+ {
+ activityStack.Children.Clear();
+ activityStack.HeightRequest = activityList.Count * 148;
+
+ activityList.ForEach((Activity activity) =>
+ {
+ activityStack.Children.Add(LabelFactory(activity));
+ }
+ );
+ }
+
+ void OnSelectTapped(object sender, EventArgs args)
+ {
+ Activity focusedActivity = (activityStack.Children[focusedIndex] as ActivityLabel).LabelActivity;
+
+ setupPageModel.updateCurrentActivity(focusedActivity);
+ PopSelectPages();
+ }
+
+ async private void Activity_Selected(object sender, EventArgs e)
+ {
+ Activity tappedActivity = ((ActivityLabel)sender).LabelActivity;
+
+ if (tappedActivity.id == -1) // Not an activity but a holder
+ {
+ await Navigation.PushAsync(new SelectWorkoutPage(setupPageModel, tappedActivity.group_id));
+ }
+ else
+ {
+ setupPageModel.updateCurrentActivity(tappedActivity);
+ PopSelectPages();
+ }
+ }
+
+ async private void PopSelectPages()
+ {
+ int pageIndex = Navigation.NavigationStack.Count - 2;
+
+ while (Navigation.NavigationStack[pageIndex].GetType().Equals(this.GetType()))
+ {
+ if (pageIndex <= 0) break; // do not remove root page
+
+ Navigation.RemovePage(Navigation.NavigationStack[pageIndex]);
+ pageIndex--;
+ }
+
+ await Navigation.PopAsync();
+ }
+
+ void OnScrolled(object sender, ScrolledEventArgs e) => Scroller(e.ScrollY);
+
+ void Scroller(double y)
+ {
+ lastScrollPos = y;
+ focusedIndex = 0;
+ double focusedDistance = 100;
+
+ for (int i = 0; i < activityStack.Children.Count; i++)
+ {
+ Label activityLabel = activityStack.Children[i] as Label;
+
+ double distanceFromCenter = i == 0 ? y : Math.Abs(y - ((i * SCROLL_INTERVAL)));
+
+ double scale = MAX_SCALE - (distanceFromCenter / 750);
+
+ activityLabel.Scale = scale < MIN_SCALE ? MIN_SCALE
+ : scale > MAX_SCALE ? MAX_SCALE
+ : scale;
+
+ if (distanceFromCenter < focusedDistance)
+ {
+ focusedDistance = distanceFromCenter;
+ focusedIndex = i;
+ }
+ activityLabel.TextColor = Color.White;
+ }
+
+ isScrolling = true;
+ DetectEndOfScrolling();
+ }
+
+ /// <summary>
+ /// Determines that scrolling has ended (and stops polling) if isScrolling flag stays false at the end of timer,
+ /// and afterwards triggers focusing of current item to center of screen.
+ /// </summary>
+ private void DetectEndOfScrolling()
+ {
+ if (isTimerRunning)
+ return;
+
+ isTimerRunning = true;
+ Device.StartTimer(TimeSpan.FromMilliseconds(100), () => {
+ if (!isScrolling)
+ {
+ ActivityLabel focusedLabel = activityStack.Children[focusedIndex] as ActivityLabel;
+ double finalPos = focusedIndex * SCROLL_INTERVAL;
+
+ scroller.ScrollToAsync(0, finalPos, true);
+
+ if (lastScrollPos == finalPos || focusedIndex == activityStack.Children.Count - 1)
+ {
+ focusedLabel.TextColor = Color.FromHex($"0CAEFF");
+ }
+
+ selectBtn.IsVisible = focusedLabel.LabelActivity.id != -1;
+ isTimerRunning = false;
+ return false;
+ }
+ else
+ {
+ isScrolling = false;
+ return true;
+ }
+ });
+ }
+ }
+
+ // Class to attach Activity Object to label. Workaround to identify Activity clicked by user.
+ class ActivityLabel : Label
+ {
+ public Activity LabelActivity { get; set; }
+
+ public ActivityLabel(Activity activity) : base()
+ {
+ LabelActivity = activity;
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8" ?>
+<c:CirclePage xmlns="http://xamarin.com/schemas/2014/forms"
+ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+ xmlns:c="clr-namespace:Tizen.Wearable.CircularUI.Forms;assembly=XSF"
+ x:Class="MapMyRun.Tizen.Views.SetDurationPage"
+ xmlns:local="clr-namespace:MapMyRun.Tizen.Views.Templates;assembly=MapMyRun.Tizen"
+ RotaryFocusObject="{x:Reference durationMenu}"
+ NavigationPage.HasNavigationBar="False">
+ <c:CirclePage.Resources>
+ <ResourceDictionary>
+ <DataTemplate x:Key="textCellTemplate">
+ <ViewCell>
+ <StackLayout>
+ <Label Text="{Binding Name}"
+ Style="{StaticResource PrimaryCellLabelStyle}"
+ />
+ <Label Text="{Binding SubText}"
+ IsVisible="{Binding HasSubText}"
+ Style="{StaticResource DetailCellLabelStyle}"
+ />
+ </StackLayout>
+ </ViewCell>
+ </DataTemplate>
+ <DataTemplate x:Key="switchCellTemplate">
+ <ViewCell>
+ <StackLayout HeightRequest="120" WidthRequest="200" Orientation="Horizontal">
+ <Label Text="{Binding Name}"
+ VerticalTextAlignment="Center"
+ HorizontalOptions="CenterAndExpand"/>
+ <c:Radio GroupName="DurationGroup"
+ IsSelected="{Binding RadioSelected}"
+ Selected="OnDurationChanged"
+ Value="{Binding RadioValue}"/>
+ </StackLayout>
+ </ViewCell>
+ </DataTemplate>
+ <local:SettingCellTemplateSelector x:Key="settingCellTemplateSelector"
+ TextCellTemplate="{StaticResource textCellTemplate}"
+ SwitchCellTemplate="{StaticResource switchCellTemplate}"
+ />
+ </ResourceDictionary>
+ </c:CirclePage.Resources>
+ <c:CirclePage.Content>
+ <c:CircleListView x:Name="durationMenu"
+ HasUnevenRows="True"
+ ItemTapped="OnItemTapped"
+ ItemTemplate="{StaticResource settingCellTemplateSelector}">
+ <c:CircleListView.Header>
+ <x:String>Display Time</x:String>
+ </c:CircleListView.Header>
+ <c:CircleListView.HeaderTemplate>
+ <DataTemplate>
+ <Label
+ c:CircleListView.CancelEffect="True"
+ HeightRequest="120"
+ Text="{Binding .}"
+ Style="{StaticResource PrimaryHeaderStyle}" />
+ </DataTemplate>
+ </c:CircleListView.HeaderTemplate>
+ </c:CircleListView>
+ </c:CirclePage.Content>
+</c:CirclePage>
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Xamarin.Forms;
+using Xamarin.Forms.Xaml;
+using Tizen.Wearable.CircularUI.Forms;
+using MapMyRun.Tizen.Views.Templates;
+using MapMyRun.Tizen.ViewModels;
+
+namespace MapMyRun.Tizen.Views
+{
+ [XamlCompilation(XamlCompilationOptions.Compile)]
+ public partial class SetDurationPage : CirclePage
+ {
+ public SettingCell longDurationCell;
+
+ public SettingCell midDurationCell;
+
+ public SettingCell shortDurationCell;
+
+ public ObservableCollection<SettingCell> settingsList;
+
+ private SettingsPageModel pageViewModel;
+
+ public SetDurationPage()
+ {
+ InitializeComponent();
+ pageViewModel = SettingsPageModel.GetInstance();
+ BindingContext = pageViewModel;
+ settingsList = new ObservableCollection<SettingCell>();
+ durationMenu.ItemsSource = settingsList;
+
+ CreateSettingCells();
+ UpdateSettingsCells();
+ PopulateSettingsList();
+ }
+
+ protected override void OnAppearing()
+ {
+ UpdateSettingsCells();
+ }
+
+ private void CreateSettingCells()
+ {
+ longDurationCell = new SettingCell("16 seconds", "", true);
+ longDurationCell.RadioValue = "16 seconds";
+
+ midDurationCell = new SettingCell("12 seconds", "", true);
+ midDurationCell.RadioValue = "12 seconds";
+
+ shortDurationCell = new SettingCell("6 seconds", "", true);
+ shortDurationCell.RadioValue = "6 seconds";
+ }
+
+ private void UpdateSettingsCells()
+ {
+ longDurationCell.RadioSelected = (pageViewModel.DisplayTime == 16);
+ midDurationCell.RadioSelected = (pageViewModel.DisplayTime == 12);
+ shortDurationCell.RadioSelected = (pageViewModel.DisplayTime == 6);
+ }
+
+ private void PopulateSettingsList()
+ {
+ settingsList.Add(longDurationCell);
+ settingsList.Add(midDurationCell);
+ settingsList.Add(shortDurationCell);
+ }
+
+ public void OnItemTapped(object sender, ItemTappedEventArgs args)
+ {
+ SettingCell clickedCell = (SettingCell) args.Item;
+
+ Console.WriteLine($"OnItemTapped Item:{clickedCell.Name}");
+ }
+
+ public void OnDurationChanged(object sender, CheckedChangedEventArgs args)
+ {
+ try
+ {
+ Radio radioItem = sender as Radio;
+
+ if (radioItem.Value == null) return;
+
+ switch (radioItem.Value)
+ {
+ case "6 seconds":
+ pageViewModel.DisplayTime = 6;
+ break;
+ case "12 seconds":
+ pageViewModel.DisplayTime = 12;
+ break;
+ case "16 seconds":
+ default:
+ pageViewModel.DisplayTime = 16;
+ break;
+
+ }
+ }
+ catch (NullReferenceException e)
+ {
+ Console.WriteLine($"Empty sender. Will not update Display Time.");
+ }
+
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8" ?>
+<c:CirclePage xmlns="http://xamarin.com/schemas/2014/forms"
+ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+ xmlns:c="clr-namespace:Tizen.Wearable.CircularUI.Forms;assembly=XSF"
+ x:Class="MapMyRun.Tizen.Views.SettingsPage"
+ xmlns:local="clr-namespace:MapMyRun.Tizen.Views.Templates;assembly=MapMyRun.Tizen"
+ RotaryFocusObject="{x:Reference SettingsList}"
+ NavigationPage.HasNavigationBar="False">
+ <c:CirclePage.Resources>
+ <ResourceDictionary>
+ <DataTemplate x:Key="textCellTemplate">
+ <ViewCell>
+ <StackLayout>
+ <Label Text="{Binding Name}"
+ Style="{StaticResource PrimaryCellLabelStyle}"
+ />
+ <Label Text="{Binding SubText}"
+ IsVisible="{Binding HasSubText}"
+ Style="{StaticResource DetailCellLabelStyle}"
+ />
+ </StackLayout>
+ </ViewCell>
+ </DataTemplate>
+ <DataTemplate x:Key="switchCellTemplate">
+ <SwitchCell
+ Text="{Binding Name}"
+ On="{Binding RadioSelected}"
+ OnChanged="OnGPSToggled"
+ />
+ </DataTemplate>
+ <local:SettingCellTemplateSelector x:Key="settingCellTemplateSelector"
+ TextCellTemplate="{StaticResource textCellTemplate}"
+ SwitchCellTemplate="{StaticResource switchCellTemplate}"
+ />
+ </ResourceDictionary>
+ </c:CirclePage.Resources>
+ <c:CirclePage.Content>
+ <c:CircleListView x:Name="SettingsList"
+ ItemTapped="OnItemTapped"
+ ItemTemplate="{StaticResource settingCellTemplateSelector}">
+ <c:CircleListView.Header>
+ <x:String>Settings</x:String>
+ </c:CircleListView.Header>
+ <c:CircleListView.HeaderTemplate>
+ <DataTemplate>
+ <Label
+ c:CircleListView.CancelEffect="True"
+ HeightRequest="120"
+ Text="{Binding .}"
+ Style="{StaticResource PrimaryHeaderStyle}" />
+ </DataTemplate>
+ </c:CircleListView.HeaderTemplate>
+ </c:CircleListView>
+ </c:CirclePage.Content>
+</c:CirclePage>
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Xamarin.Forms;
+using Xamarin.Forms.Xaml;
+using Tizen.Wearable.CircularUI.Forms;
+using MapMyRun.Tizen.Views.Templates;
+using MapMyRun.Tizen.ViewModels;
+
+namespace MapMyRun.Tizen.Views
+{
+ [XamlCompilation(XamlCompilationOptions.Compile)]
+ public partial class SettingsPage : CirclePage
+ {
+ public SettingCell displayTimeCell;
+
+ public SettingCell gpsCell;
+
+ public SettingCell versionCell;
+
+ public ObservableCollection<SettingCell> settingsList;
+
+ private SettingsPageModel settingsPageModel;
+
+ public SettingsPage()
+ {
+ Console.WriteLine($"Settings Page constructor");
+ InitializeComponent();
+ settingsList = new ObservableCollection<SettingCell>();
+ settingsPageModel = SettingsPageModel.GetInstance();
+ BindingContext = settingsPageModel;
+ SettingsList.ItemsSource = settingsList;
+
+ CreateSettingCells();
+ UpdateSettingsCells();
+ PopulateSettingsList();
+ }
+
+ protected override void OnAppearing()
+ {
+ UpdateSettingsCells();
+ }
+
+ private void CreateSettingCells()
+ {
+ displayTimeCell = new SettingCell("Display Time");
+ gpsCell = new SettingCell("GPS", "", true);
+ versionCell = new SettingCell("v2.4.4");
+ }
+
+ private void UpdateSettingsCells()
+ {
+ displayTimeCell.SubText = $"{settingsPageModel.DisplayTime} seconds";
+ gpsCell.RadioSelected = settingsPageModel.GPSStatus;
+ }
+
+ private void PopulateSettingsList()
+ {
+ settingsList.Add(displayTimeCell);
+ settingsList.Add(gpsCell);
+ settingsList.Add(versionCell);
+ }
+
+ async public void OnItemTapped(object sender, ItemTappedEventArgs args)
+ {
+ SettingCell clickedCell = (SettingCell) args.Item;
+
+ Console.WriteLine($"OnItemTapped Item:{clickedCell.Name}");
+
+ if (clickedCell.Name.Equals("Display Time"))
+ {
+ await Navigation.PushAsync(new SetDurationPage());
+ }
+ }
+
+ public void OnGPSToggled(object sender, CheckedChangedEventArgs args)
+ {
+ SwitchCell switchCell = sender as SwitchCell;
+ try
+ {
+ if (switchCell.Text.Equals("GPS"))
+ {
+ settingsPageModel.GPSStatus = switchCell.On;
+ }
+ } catch (NullReferenceException e)
+ {
+ Console.WriteLine($"Empty sender. Will not update GPS status.");
+ }
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+
+namespace MapMyRun.Tizen.Views.Templates
+{
+ public class SettingCell : INotifyPropertyChanged
+ {
+ public string Name { get; set; }
+
+ private string _subText;
+
+ public string SubText {
+ get
+ {
+ return _subText;
+ }
+ set
+ {
+ _subText = value;
+ OnPropertyChanged();
+ }
+ }
+
+ private bool _radioSelected;
+
+ public bool RadioSelected
+ {
+ get
+ {
+ return _radioSelected;
+ }
+ set
+ {
+ _radioSelected = value;
+ OnPropertyChanged();
+ }
+ }
+
+ public string RadioValue { get; set; }
+
+ public bool HasRadio { get; set; }
+
+ public SettingCell(string name, string subText = "", bool addRadio = false)
+ {
+ Name = name;
+ SubText = subText;
+ HasRadio = addRadio;
+ }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Xamarin.Forms;
+
+namespace MapMyRun.Tizen.Views.Templates
+{
+ public class SettingCellTemplateSelector : DataTemplateSelector
+ {
+ public DataTemplate TextCellTemplate { get; set; }
+ public DataTemplate SwitchCellTemplate { get; set; }
+
+ protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
+ {
+ return ((SettingCell)item).HasRadio ? SwitchCellTemplate : TextCellTemplate;
+ }
+ }
+}
--- /dev/null
+<?xml version="1.0" encoding="utf-8" ?>
+<c:CirclePage
+ xmlns="http://xamarin.com/schemas/2014/forms"
+ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+ xmlns:c="clr-namespace:Tizen.Wearable.CircularUI.Forms;assembly=XSF"
+ x:Class="MapMyRun.Tizen.Views.WorkoutHistoryPage"
+ NavigationPage.HasNavigationBar="False"
+ RotaryFocusObject="{Reference myWorkouts}">
+ <c:CirclePage.Content>
+ <c:CircleListView
+ x:Name="myWorkouts"
+ GroupDisplayBinding="{Binding Name}"
+ IsGroupingEnabled="True"
+ ItemsSource="{Binding .}"
+ Header="My Workouts">
+ <c:CircleListView.ItemTemplate>
+ <DataTemplate>
+ <ViewCell>
+ <StackLayout>
+ <Label
+ Text="{Binding Category}"
+ Style="{StaticResource PrimaryCellLabelStyle}"/>
+ <Label
+ Text="{Binding Selected}"
+ Style="{StaticResource DetailCellLabelStyleCloserMargin}"/>
+ </StackLayout>
+ </ViewCell>
+ </DataTemplate>
+ </c:CircleListView.ItemTemplate>
+ <c:CircleListView.HeaderTemplate>
+ <DataTemplate>
+ <Label
+ c:CircleListView.CancelEffect="True"
+ HeightRequest="120"
+ Text="{Binding .}"
+ Style="{StaticResource PrimaryHeaderStyle}"/>
+ </DataTemplate>
+ </c:CircleListView.HeaderTemplate>
+ </c:CircleListView>
+ </c:CirclePage.Content>
+</c:CirclePage>
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Tizen.Wearable.CircularUI.Forms;
+using Xamarin.Forms;
+using Xamarin.Forms.Xaml;
+
+namespace MapMyRun.Tizen.Views
+{
+ [XamlCompilation(XamlCompilationOptions.Compile)]
+ public partial class WorkoutHistoryPage : CirclePage
+ {
+ // dummy data class
+ public class GroupModel : List<NamedList<WorkoutSetupEntry>>
+ {
+ public GroupModel()
+ {
+ Add(new NamedList<WorkoutSetupEntry>("THIS WEEK") { new WorkoutSetupEntry() { Category="Intervals", Selected="11/27/2019 0:01 min" } });
+ Add(new NamedList<WorkoutSetupEntry>("OLDER") { new WorkoutSetupEntry() { Category="Run, on 11/13/2019", Selected="11/13/2019 0:04 min" } });
+ }
+ }
+
+ public class NamedList<T> : List<T>
+ {
+ public NamedList(string name) => Name = name;
+ public string Name { get; set; }
+ }
+
+ public class WorkoutSetupEntry
+ {
+ public string Category
+ {
+ set; get;
+ }
+
+ public string Selected
+ {
+ set; get;
+ }
+ }
+
+ public WorkoutHistoryPage()
+ {
+ InitializeComponent();
+ BindingContext = new GroupModel();
+
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<c:IndexPage xmlns="http://xamarin.com/schemas/2014/forms"
+ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+ xmlns:c="clr-namespace:Tizen.Wearable.CircularUI.Forms;assembly=XSF"
+ x:Class="MapMyRun.Tizen.Views.WorkoutMainIndexPage"
+ NavigationPage.HasNavigationBar="False">
+</c:IndexPage>
\ No newline at end of file
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Tizen.Wearable.CircularUI.Forms;
+using Xamarin.Forms;
+using Xamarin.Forms.Xaml;
+
+namespace MapMyRun.Tizen.Views
+{
+ [XamlCompilation(XamlCompilationOptions.Compile)]
+ public partial class WorkoutMainIndexPage : IndexPage
+ {
+ const int LEFT = 1;
+ const int RIGHT = -1;
+
+ WorkoutMainPage _workoutIndexEntry1;
+ WorkoutMainPage _workoutIndexEntry2;
+ WorkoutMainPage _workoutIndexEntry3;
+ WorkoutMainPageGPS _workoutIndexEntry4;
+
+ public WorkoutMainIndexPage()
+ {
+ InitializeComponent();
+ _workoutIndexEntry1 = new WorkoutMainPage(0);
+ _workoutIndexEntry2 = new WorkoutMainPage(1);
+ _workoutIndexEntry3 = new WorkoutMainPage(2);
+ _workoutIndexEntry4 = new WorkoutMainPageGPS();
+
+ // workoutIndexEntry2.TranslationX = 400;
+ // workoutIndexEntry3.TranslationX = 400;
+ // workoutIndexEntry4.TranslationX = 400;
+
+ _workoutIndexEntry1.RaiseRotaryEvent += RotateIndex;
+ _workoutIndexEntry2.RaiseRotaryEvent += RotateIndex;
+ _workoutIndexEntry3.RaiseRotaryEvent += RotateIndex;
+ _workoutIndexEntry4.RaiseRotaryEvent += RotateIndex;
+
+ //_workoutIndexEntry1.RaiseBackButtonPressed += EnableSwitchPage;
+ //_workoutIndexEntry2.RaiseBackButtonPressed += EnableSwitchPage;
+ //_workoutIndexEntry3.RaiseBackButtonPressed += EnableSwitchPage;
+ //_workoutIndexEntry4.RaiseBackButtonPressed += EnableSwitchPage;
+
+ _workoutIndexEntry1.RaiseBackButtonPressed += ToggleTimer;
+ _workoutIndexEntry2.RaiseBackButtonPressed += ToggleTimer;
+ _workoutIndexEntry3.RaiseBackButtonPressed += ToggleTimer;
+ _workoutIndexEntry4.RaiseBackButtonPressed += ToggleTimer;
+
+ Children.Add(_workoutIndexEntry1);
+ Children.Add(_workoutIndexEntry2);
+ Children.Add(_workoutIndexEntry3);
+ Children.Add(_workoutIndexEntry4);
+
+ CurrentPageChanged += BackgroundOpacityChange;
+ }
+
+ public /*async*/ void RotateIndex(object sender, RotaryEventArgs args)
+ {
+ int direction = args.IsClockwise ? LEFT: RIGHT;
+ int index = Children.IndexOf(CurrentPage);
+ if (index + direction < 0 || index + direction > 3) return;
+ //CurrentPage.TranslateTo(400*-direction, CurrentPage.TranslationY, 300);
+ //Task.Delay(100);
+ CurrentPage = Children.ElementAt(index + direction);
+ //await Children.ElementAt(index + direction).TranslateTo(0, Children.ElementAt(index + direction).TranslationY, 200);
+ }
+
+ //void EnableSwitchPage(bool buttonOn)
+ //{
+ // if (buttonOn) this.InputTransparent = true;
+ // else this.InputTransparent = false;
+ //}
+
+ void ToggleTimer(bool buttonOn)
+ {
+ _workoutIndexEntry1.TimerON(buttonOn);
+ }
+
+ void BackgroundOpacityChange(object sender, EventArgs args)
+ {
+ int pageNumber = Children.IndexOf(CurrentPage) + 1;
+ foreach (var page in Children)
+ {
+ if(page is WorkoutMainPage p1)
+ {
+ if(pageNumber == 2)
+ {
+ p1.BackgroundBottomFade(on:true);
+ p1.BackgroundWholeFade(on:false);
+ }
+ else if(pageNumber == 3)
+ {
+ p1.BackgroundBottomFade(on:false);
+ p1.BackgroundWholeFade(on:true);
+ }
+ else if(pageNumber == 4)
+ {
+ p1.BackgroundBottomFade(on:false);
+ p1.BackgroundWholeFade(on:false);
+ }
+ }
+ else if (page is WorkoutMainPageGPS p2)
+ {
+ if (pageNumber == 2)
+ {
+ p2.BackgroundBottomFade(on: true);
+ p2.BackgroundWholeFade(on: false);
+ }
+ else if (pageNumber == 3)
+ {
+ p2.BackgroundBottomFade(on: false);
+ p2.BackgroundWholeFade(on: true);
+ }
+ else if (pageNumber == 4)
+ {
+ p2.BackgroundBottomFade(on: false);
+ p2.BackgroundWholeFade(on: false);
+ }
+ }
+ }
+ }
+ }
+
+}
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<c:CirclePage xmlns="http://xamarin.com/schemas/2014/forms"
+ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+ xmlns:c="clr-namespace:Tizen.Wearable.CircularUI.Forms;assembly=XSF"
+ x:Class="MapMyRun.Tizen.Views.WorkoutMainLoadingPage"
+ NavigationPage.HasNavigationBar="False">
+ <c:CirclePage.CircleSurfaceItems>
+ <c:CircleProgressBarSurfaceItem
+ x:Name="progress"
+ IsVisible="True"
+ />
+ </c:CirclePage.CircleSurfaceItems>
+
+ <c:CirclePage.Content>
+ <Label
+ x:Name="label"
+ Text="{Binding .}"
+ HorizontalTextAlignment="Center"
+ VerticalTextAlignment="Center"
+ />
+ </c:CirclePage.Content>
+</c:CirclePage>
\ No newline at end of file
--- /dev/null
+using MapMyRun.Models.Workout;
+using MapMyRun.Models.Workout.Items;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Tizen.Wearable.CircularUI.Forms;
+using Xamarin.Forms;
+using Xamarin.Forms.Xaml;
+
+namespace MapMyRun.Tizen.Views
+{
+ [XamlCompilation(XamlCompilationOptions.Compile)]
+ public partial class WorkoutMainLoadingPage : CirclePage
+ {
+ const int COUNT_THREE_SECONDS = 3;
+ const int COUNT_UA_ICON_MOMENT = 0;
+ const double ZERO_PROGRESS_BAR = 0.0;
+ const double HALF_PROGRESS_BAR = 0.5;
+ const double FULL_PROGRESS_BAR = 1.0;
+ const double ZERO_OPACITY = 0.0;
+ const double FULL_OPACITY = 1.0;
+ const double PROGRESS_INCREASE_STEP = 0.05;
+ const double OPACITY_INCREASE_STEP = 0.1;
+ const int FONT_SIZE_INCREASE_STEP = 2;
+ const int INIT_FONTSIZE = 50;
+
+ Image image = new Image()
+ {
+ Source = "UAIcon.png",
+ HorizontalOptions = LayoutOptions.Center,
+ VerticalOptions = LayoutOptions.Center,
+ Opacity = ZERO_OPACITY
+ };
+
+ public WorkoutMainLoadingPage()
+ {
+ InitializeComponent();
+
+ int counter = COUNT_THREE_SECONDS;
+ label.Opacity = ZERO_OPACITY;
+ label.FontSize = INIT_FONTSIZE;
+ label.Text = counter.ToString();
+
+ // animation moves every 50ms, counter decreases every second
+ // hence, 20 loops for each second
+ // 3 -> 2 -> 1 -> UnderArmor Icon -> UnderArmor Icon gets bigger and fades out
+ Device.StartTimer(TimeSpan.FromMilliseconds(50), () =>
+ {
+ if(progress.Value == FULL_PROGRESS_BAR)
+ {
+ --counter;
+ if(counter != COUNT_UA_ICON_MOMENT)
+ {
+ label.Text = counter.ToString();
+ label.FontSize = INIT_FONTSIZE;
+ label.Opacity = ZERO_OPACITY;
+ progress.Value = ZERO_PROGRESS_BAR;
+ }
+ else
+ {
+ this.Content = image;
+ UAIcon();
+ return false;
+ }
+ }
+ if(progress.Value <= HALF_PROGRESS_BAR)
+ {
+ label.FontSize += FONT_SIZE_INCREASE_STEP;
+ label.Opacity += OPACITY_INCREASE_STEP;
+ }
+ progress.Value += PROGRESS_INCREASE_STEP;
+ return true;
+ });
+
+ }
+
+ async void UAIcon()
+ {
+ image.FadeTo(FULL_OPACITY, 500);
+ await image.ScaleTo(1.5, 500);
+ await Task.Delay(500);
+ image.FadeTo(ZERO_OPACITY, 300);
+ await image.ScaleTo(10, 300);
+ Application.Current.MainPage = new WorkoutMainIndexPage();
+
+ // TODO : Move proper position
+ Workout workout = (Application.Current as App).workout;
+ workout.StartWorkout();
+ return;
+ }
+
+ }
+}
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8" ?>
+<c:CirclePage
+ xmlns="http://xamarin.com/schemas/2014/forms"
+ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+ xmlns:c="clr-namespace:Tizen.Wearable.CircularUI.Forms;assembly=XSF"
+ x:Class="MapMyRun.Tizen.Views.WorkoutMainPage"
+ NavigationPage.HasNavigationBar="False"
+ x:Name="WorkoutIndexEntry"
+ RotaryFocusObject="{x:Reference WorkoutIndexEntry}">
+ <c:CirclePage.Content>
+ <RelativeLayout x:Name="layout">
+ </RelativeLayout>
+ </c:CirclePage.Content>
+</c:CirclePage>
--- /dev/null
+using MapMyRun.Models.Workout;
+using MapMyRun.Tizen.ViewModels.WorkoutMain;
+using System;
+using Tizen.Wearable.CircularUI.Forms;
+using Xamarin.Forms;
+using Xamarin.Forms.Xaml;
+
+namespace MapMyRun.Tizen.Views
+{
+ [XamlCompilation(XamlCompilationOptions.Compile)]
+ public partial class WorkoutMainPage : CirclePage, IRotaryEventReceiver
+ {
+ const int MAINCELL_SIZE = 22;
+ const int SPACING = 5;
+ const int TOP_SPACING = 60;
+ const int HEART_SIZE = 20;
+ const double UNPRESSED_OPACITY = 1.0;
+ const double PRESSED_OPACITY = 0.5;
+ Color TRANSPARENT_BACKGROUND = new Color(0, 0, 0, 0);
+ const double BUTTON_WIDTH_COEFFICIENT = 0.8;
+ const double BUTTON_HEIGHT_COEFFICIENT = 0.27;
+ const double BUTTON_XPOSITION_COEFFICIENT = 0.1;
+ const double BUTTON_YPOSITION_COEFFICIENT = 0.65;
+ const int BACKGROUND_FADE_TIME = 300;
+ const double FULL_OPACITY = 1.0;
+ const double ZERO_OPACITY = 0.0;
+ const double SUBCELL_WIDTH_COEFFICIENT = 0.45;
+ const double RIGHTSUBCELL_XPOSITION_COEFFICIENT = 0.55;
+ const double BOTTOMCELL_WIDTH_COEFFICIENT = 0.2;
+ const double BOTTOMCELL_XPOSITION_COEFFICIENT = 0.4;
+
+ bool buttonON;
+
+ public TapGestureRecognizer recognizer = new TapGestureRecognizer();
+
+ public event EventHandler<RotaryEventArgs> RaiseRotaryEvent;
+ public Action<bool> RaiseBackButtonPressed;
+
+ // Controls
+ Label MainCell, SubCellLeft, SubCellRight, MainLabel, SubLabelLeft, SubLabelRight, BottomLabel;
+ Image BottomCell, BackgroundBottomLight, BackgroundWholeLight;
+ ImageButton FinishButton;
+
+ WorkoutMainPageModel _workoutMainPageModel;
+ int _pageNumber;
+
+ public WorkoutMainPage(int pageNumber)
+ {
+ InitializeComponent();
+
+ _pageNumber = pageNumber;
+ _workoutMainPageModel = new WorkoutMainPageModel((Application.Current as App).workout);
+
+ // Controls
+ BackgroundBottomLight = new Image()
+ {
+ Source = "backgroundBottomLight.png",
+ Opacity = FULL_OPACITY
+ };
+
+ BackgroundWholeLight = new Image()
+ {
+ Source = "backgroundWholeLight.png",
+ Opacity = ZERO_OPACITY
+ };
+
+ MainCell = new Label()
+ {
+ HorizontalTextAlignment = TextAlignment.Center,
+ VerticalTextAlignment = TextAlignment.Center,
+ FontSize = MAINCELL_SIZE,
+ BackgroundColor = TRANSPARENT_BACKGROUND,
+ TextColor = Color.White
+ };
+ MainCell.SetBinding(Label.TextProperty, nameof(IWorkoutItemViewModel.Value));
+
+ MainLabel = new Label()
+ {
+ HorizontalTextAlignment = TextAlignment.Center,
+ VerticalTextAlignment = TextAlignment.Center,
+ FontSize = Device.GetNamedSize(NamedSize.Micro, typeof(Label)),
+ BackgroundColor = TRANSPARENT_BACKGROUND,
+ TextColor = Color.White
+ };
+ MainLabel.SetBinding(Label.TextProperty, nameof(IWorkoutItemViewModel.LongLabel));
+
+ SubCellLeft = new Label()
+ {
+ HorizontalTextAlignment = TextAlignment.End,
+ VerticalTextAlignment = TextAlignment.Center,
+ FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)),
+ BackgroundColor = TRANSPARENT_BACKGROUND,
+ TextColor = Color.White
+ };
+ SubCellLeft.SetBinding(Label.TextProperty, nameof(IWorkoutItemViewModel.Value));
+
+ SubLabelLeft = new Label()
+ {
+ HorizontalTextAlignment = TextAlignment.End,
+ VerticalTextAlignment = TextAlignment.Center,
+ FontSize = Device.GetNamedSize(NamedSize.Micro, typeof(Label)),
+ BackgroundColor = TRANSPARENT_BACKGROUND,
+ TextColor = Color.White
+ };
+ SubLabelLeft.SetBinding(Label.TextProperty, nameof(IWorkoutItemViewModel.MediumLabel));
+
+ SubCellRight = new Label()
+ {
+ HorizontalTextAlignment = TextAlignment.Start,
+ VerticalTextAlignment = TextAlignment.Center,
+ FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)),
+ BackgroundColor = TRANSPARENT_BACKGROUND,
+ TextColor = Color.White
+ };
+ SubCellRight.SetBinding(Label.TextProperty, nameof(IWorkoutItemViewModel.Value));
+
+ SubLabelRight = new Label()
+ {
+ HorizontalTextAlignment = TextAlignment.Start,
+ VerticalTextAlignment = TextAlignment.Center,
+ FontSize = Device.GetNamedSize(NamedSize.Micro, typeof(Label)),
+ BackgroundColor = TRANSPARENT_BACKGROUND,
+ TextColor = Color.White
+ };
+ SubLabelRight.SetBinding(Label.TextProperty, nameof(IWorkoutItemViewModel.MediumLabel));
+
+ BottomCell = new Image()
+ {
+ Source = "BPMIcon.png",
+ HeightRequest = HEART_SIZE,
+ WidthRequest = HEART_SIZE,
+ BackgroundColor = TRANSPARENT_BACKGROUND
+ };
+
+ BottomLabel = new Label()
+ {
+ HorizontalTextAlignment = TextAlignment.Center,
+ VerticalTextAlignment = TextAlignment.Center,
+ FontSize = Device.GetNamedSize(NamedSize.Micro, typeof(Label)),
+ BackgroundColor = TRANSPARENT_BACKGROUND,
+ TextColor = Color.White
+ };
+ BottomLabel.SetBinding(Label.TextProperty, nameof(IWorkoutItemViewModel.ValueWithUnit));
+
+ FinishButton = new ImageButton()
+ {
+ Source = ImageSource.FromFile("finish-btn.png")
+ };
+ FinishButton.Pressed += (s, e) => {
+ FinishButton.Opacity = PRESSED_OPACITY;
+ };
+ FinishButton.Released += (s, e) =>
+ {
+ FinishButton.Opacity = UNPRESSED_OPACITY;
+ };
+
+ buttonON = false;
+ MainCell.BindingContext = _workoutMainPageModel.GetWorkoutViewModel(pageNumber, 0);
+ MainLabel.BindingContext = _workoutMainPageModel.GetWorkoutViewModel(pageNumber, 0);
+ SubCellLeft.BindingContext = _workoutMainPageModel.GetWorkoutViewModel(pageNumber, 1);
+ SubLabelLeft.BindingContext = _workoutMainPageModel.GetWorkoutViewModel(pageNumber, 1);
+ SubCellRight.BindingContext = _workoutMainPageModel.GetWorkoutViewModel(pageNumber, 2);
+ SubLabelRight.BindingContext = _workoutMainPageModel.GetWorkoutViewModel(pageNumber, 2);
+ BottomCell.BindingContext = _workoutMainPageModel.GetWorkoutViewModel(pageNumber, 3);
+ BottomLabel.BindingContext = _workoutMainPageModel.GetWorkoutViewModel(pageNumber, 3);
+
+ layout.Children.Add(BackgroundBottomLight,
+ heightConstraint: Constraint.RelativeToParent((parent) => { return parent.Height;}),
+ widthConstraint: Constraint.RelativeToParent((parent) => { return parent.Width; }));
+
+ layout.Children.Add(BackgroundWholeLight,
+ heightConstraint: Constraint.RelativeToParent((parent) => { return parent.Height; }),
+ widthConstraint: Constraint.RelativeToParent((parent) => { return parent.Width; }));
+
+ layout.Children.Add(MainCell,
+ yConstraint: Constraint.RelativeToParent((parent) => { return parent.Y + TOP_SPACING; }),
+ widthConstraint: Constraint.RelativeToParent((parent) => { return parent.Width; }));
+
+ layout.Children.Add(MainLabel,
+ yConstraint: Constraint.RelativeToView(MainCell, (parent, sibling) => { return sibling.Y + sibling.Height + SPACING; }),
+ widthConstraint: Constraint.RelativeToParent((parent) => { return parent.Width; }));
+
+ layout.Children.Add(SubCellLeft,
+ yConstraint: Constraint.RelativeToView(MainLabel, (parent, sibling) => { return sibling.Y + sibling.Height + 2*SPACING; }),
+ widthConstraint: Constraint.RelativeToParent((parent) => { return parent.Width * SUBCELL_WIDTH_COEFFICIENT; }));
+
+ layout.Children.Add(SubLabelLeft,
+ yConstraint: Constraint.RelativeToView(SubCellLeft, (parent, sibling) => { return sibling.Y + sibling.Height + SPACING; }),
+ widthConstraint: Constraint.RelativeToParent((parent) => { return parent.Width * SUBCELL_WIDTH_COEFFICIENT; }));
+
+ layout.Children.Add(SubCellRight,
+ xConstraint: Constraint.RelativeToParent((parent) => { return parent.Width * RIGHTSUBCELL_XPOSITION_COEFFICIENT; }),
+ yConstraint: Constraint.RelativeToView(MainLabel, (parent, sibling) => { return sibling.Y + sibling.Height + 2*SPACING; }),
+ widthConstraint: Constraint.RelativeToParent((parent) => { return parent.Width * SUBCELL_WIDTH_COEFFICIENT; }));
+
+ layout.Children.Add(SubLabelRight,
+ xConstraint: Constraint.RelativeToParent((parent) => { return parent.Width * RIGHTSUBCELL_XPOSITION_COEFFICIENT; }),
+ yConstraint: Constraint.RelativeToView(SubCellRight, (parent, sibling) => { return sibling.Y + sibling.Height + SPACING; }),
+ widthConstraint: Constraint.RelativeToParent((parent) => { return parent.Width * SUBCELL_WIDTH_COEFFICIENT; }));
+
+ layout.Children.Add(BottomCell,
+ xConstraint: Constraint.RelativeToParent((parent) => { return parent.Width * BOTTOMCELL_XPOSITION_COEFFICIENT; }),
+ yConstraint: Constraint.RelativeToView(SubLabelLeft, (parent, sibling) => { return sibling.Y + sibling.Height + 2*SPACING; }),
+ widthConstraint: Constraint.RelativeToParent((parent) => { return parent.Width * BOTTOMCELL_WIDTH_COEFFICIENT; }));
+
+ layout.Children.Add(BottomLabel,
+ yConstraint: Constraint.RelativeToView(BottomCell, (parent, sibling) => { return sibling.Y + sibling.Height + SPACING; }),
+ widthConstraint: Constraint.RelativeToParent((parent) => { return parent.Width; }));
+
+ recognizer.Tapped += OnPageClick;
+ this.Content.GestureRecognizers.Add(recognizer);
+
+ FinishButton.Clicked += (s, e) =>
+ {
+ Workout workout = (Application.Current as App).workout;
+ workout.FinishWorkout();
+ Application.Current.MainPage = new NavigationPage(new MainPage());
+ };
+ }
+
+ void OnPageClick(object s, EventArgs e)
+ {
+ var tempName = MainCell.BindingContext;
+ var tempVal = MainLabel.BindingContext;
+
+ MainCell.BindingContext = SubCellLeft.BindingContext;
+ MainLabel.BindingContext = SubLabelLeft.BindingContext;
+ SubCellLeft.BindingContext = SubCellRight.BindingContext;
+ SubLabelLeft.BindingContext = SubLabelRight.BindingContext;
+ SubCellRight.BindingContext = tempName;
+ SubLabelRight.BindingContext = tempVal;
+ }
+
+ public void TimerON(bool on)
+ {
+ // pageNumber 0, item 0 has DurationViewModel
+ IWorkoutItemViewModel itemViewModel = _workoutMainPageModel.GetWorkoutViewModel(0, 0);
+ if(itemViewModel is DurationViewModel _duration)
+ {
+ if (on) _duration.StopTimer();
+ else _duration.ResumeTimer();
+ }
+ }
+
+ protected override bool OnBackButtonPressed()
+ {
+ if (buttonON == false)
+ {
+ MainCell.FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label));
+ Content.GestureRecognizers.Remove(recognizer);
+ BottomCell.IsVisible = false;
+ BottomLabel.IsVisible = false;
+ layout.Children.Add(FinishButton,
+ widthConstraint: Constraint.RelativeToParent((parent) => { return parent.Width * BUTTON_WIDTH_COEFFICIENT; }),
+ heightConstraint: Constraint.RelativeToParent((parent) => { return parent.Height * BUTTON_HEIGHT_COEFFICIENT; }),
+ xConstraint: Constraint.RelativeToParent((parent) => { return parent.Width * BUTTON_XPOSITION_COEFFICIENT; }),
+ yConstraint: Constraint.RelativeToParent((parent) => { return parent.Height * BUTTON_YPOSITION_COEFFICIENT; }));
+ buttonON = true;
+ RaiseBackButtonPressed?.Invoke(buttonON);
+ }
+ else
+ {
+ MainCell.FontSize = MAINCELL_SIZE;
+ Content.GestureRecognizers.Add(recognizer);
+ BottomCell.IsVisible = true;
+ BottomLabel.IsVisible = true;
+
+ layout.Children.Remove(FinishButton);
+ buttonON = false;
+ RaiseBackButtonPressed?.Invoke(buttonON);
+ }
+ return true;
+ }
+
+ public void Rotate(RotaryEventArgs args)
+ {
+ EventHandler< RotaryEventArgs> handler = RaiseRotaryEvent;
+ if(handler != null)
+ {
+ handler(this, args);
+ }
+ return;
+ }
+
+ public void BackgroundBottomFade(bool on)
+ {
+ if (on == true) BackgroundBottomLight.FadeTo(1, BACKGROUND_FADE_TIME);
+ else BackgroundBottomLight.FadeTo(FULL_OPACITY, BACKGROUND_FADE_TIME);
+ }
+
+ public void BackgroundWholeFade(bool on)
+ {
+ if (on == true) BackgroundWholeLight.FadeTo(1, BACKGROUND_FADE_TIME);
+ else BackgroundWholeLight.FadeTo(ZERO_OPACITY, BACKGROUND_FADE_TIME);
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8" ?>
+<c:CirclePage
+ xmlns="http://xamarin.com/schemas/2014/forms"
+ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+ xmlns:c="clr-namespace:Tizen.Wearable.CircularUI.Forms;assembly=XSF"
+ x:Class="MapMyRun.Tizen.Views.WorkoutMainPageGPS"
+ NavigationPage.HasNavigationBar="False"
+ x:Name="WorkoutIndexEntry"
+ RotaryFocusObject="{x:Reference WorkoutIndexEntry}">
+ <c:CirclePage.Content>
+ <RelativeLayout x:Name="layout">
+ </RelativeLayout>
+ </c:CirclePage.Content>
+</c:CirclePage>
--- /dev/null
+using System;
+using Tizen.Wearable.CircularUI.Forms;
+using Xamarin.Forms;
+using Xamarin.Forms.Xaml;
+using MapMyRun.Tizen.ViewModels.WorkoutMain;
+using MapMyRun.Models.Workout;
+
+namespace MapMyRun.Tizen.Views
+{
+ [XamlCompilation(XamlCompilationOptions.Compile)]
+ public partial class WorkoutMainPageGPS : CirclePage, IRotaryEventReceiver
+ {
+ const double ZERO_OPACITY = 0.0;
+ const double FULL_OPACITY = 1.0;
+ const double OPACITY_DECREASE_STEP = -0.1;
+ const double OPACITY_INCREASE_STEP = 0.1;
+ const double UNPRESSED_OPACITY = 1.0;
+ const double PRESSED_OPACITY = 0.5;
+ const double BUTTON_WIDTH_COEFFICIENT = 0.8;
+ const double BUTTON_HEIGHT_COEFFICIENT = 0.27;
+ const double BUTTON_XPOSITION_COEFFICIENT = 0.1;
+ const double BUTTON_YPOSITION_COEFFICIENT = 0.65;
+ const int BACKGROUND_FADE_TIME = 300;
+ const double GPSIMAGE_WIDTH_COEFFICIENT = 0.1;
+ const double GPSIMAGE_HEIGHT_COEFFICIENT = 0.1;
+ const double GPSIMAGE_XPOSITION_COEFFICIENT = 0.45;
+ const double GPSIMAGE_YPOSITION_COEFFICIENT = 0.2;
+ const double TEXT_HEIGHT_COEFFICIENT = 0.8;
+ const double TEXT_YPOSITION_COEFFICIENT = 0.35;
+
+ Label Textlabel;
+ Image GPSImage, BackgroundBottomLight, BackgroundWholeLight;
+ ImageButton FinishButton;
+
+ bool buttonON;
+ double valueChanged;
+
+ public event EventHandler<RotaryEventArgs> RaiseRotaryEvent;
+ public Action<bool> RaiseBackButtonPressed;
+ public WorkoutMainPageGPS()
+ {
+ InitializeComponent();
+
+ buttonON = false;
+ string text = "Unable to\nacquire GPS signal.\n\nPlease move to\nopen space.";
+ string fixedTest = text.Replace("\n", Environment.NewLine);
+
+ BackgroundBottomLight = new Image()
+ {
+ Source = "backgroundBottomLight.png",
+ Opacity = FULL_OPACITY
+ };
+
+ BackgroundWholeLight = new Image()
+ {
+ Source = "backgroundWholeLight.png",
+ Opacity = ZERO_OPACITY
+ };
+
+ GPSImage = new Image()
+ {
+ Source = "GPSIcon.png",
+ BackgroundColor = new Color(0, 0, 0, 0)
+ };
+
+ Textlabel = new Label()
+ {
+ Text = fixedTest,
+ TextColor = Color.White,
+ HorizontalTextAlignment = TextAlignment.Center
+ };
+
+ FinishButton = new ImageButton()
+ {
+ Source = ImageSource.FromFile("finish-btn.png")
+ };
+ FinishButton.Pressed += (s, e) => {
+ FinishButton.Opacity = PRESSED_OPACITY;
+ };
+ FinishButton.Released += (s, e) =>
+ {
+ FinishButton.Opacity = UNPRESSED_OPACITY;
+ };
+
+ layout.Children.Add(BackgroundBottomLight,
+ heightConstraint: Constraint.RelativeToParent((parent) => { return parent.Height; }),
+ widthConstraint: Constraint.RelativeToParent((parent) => { return parent.Width; }));
+
+ layout.Children.Add(BackgroundWholeLight,
+ heightConstraint: Constraint.RelativeToParent((parent) => { return parent.Height; }),
+ widthConstraint: Constraint.RelativeToParent((parent) => { return parent.Width; }));
+
+ layout.Children.Add(GPSImage,
+ heightConstraint: Constraint.RelativeToParent((parent) => { return parent.Height* GPSIMAGE_HEIGHT_COEFFICIENT; }),
+ widthConstraint: Constraint.RelativeToParent((parent) => { return parent.Width* GPSIMAGE_WIDTH_COEFFICIENT; }),
+ xConstraint: Constraint.RelativeToParent((parent) => { return parent.Width* GPSIMAGE_XPOSITION_COEFFICIENT; }),
+ yConstraint: Constraint.RelativeToParent((parent) => { return parent.Height* GPSIMAGE_YPOSITION_COEFFICIENT; }));
+
+ layout.Children.Add(Textlabel,
+ heightConstraint: Constraint.RelativeToParent((parent) => { return parent.Height* TEXT_HEIGHT_COEFFICIENT; }),
+ widthConstraint: Constraint.RelativeToParent((parent) => { return parent.Width; }),
+ yConstraint: Constraint.RelativeToParent((parent) => { return parent.Height * TEXT_YPOSITION_COEFFICIENT; }));
+
+ FinishButton.Clicked += (s, e) =>
+ {
+ Workout workout = (Application.Current as App).workout;
+ workout.FinishWorkout();
+
+ Application.Current.MainPage = new NavigationPage(new MainPage());
+ };
+
+ Device.StartTimer(TimeSpan.FromMilliseconds(100), () =>
+ {
+ if (GPSImage.Opacity == ZERO_OPACITY) valueChanged = OPACITY_INCREASE_STEP;
+ else if (GPSImage.Opacity == FULL_OPACITY) valueChanged = OPACITY_DECREASE_STEP;
+ GPSImage.Opacity += valueChanged;
+ return true;
+ }
+ );
+ }
+
+ protected override bool OnBackButtonPressed()
+ {
+ if (buttonON == false)
+ {
+ layout.Children.Add(FinishButton,
+ widthConstraint: Constraint.RelativeToParent((parent) => { return parent.Width * BUTTON_WIDTH_COEFFICIENT; }),
+ heightConstraint: Constraint.RelativeToParent((parent) => { return parent.Height* BUTTON_HEIGHT_COEFFICIENT; }),
+ xConstraint: Constraint.RelativeToParent((parent) => { return parent.Width * BUTTON_XPOSITION_COEFFICIENT; }),
+ yConstraint: Constraint.RelativeToParent((parent)=> { return parent.Height* BUTTON_YPOSITION_COEFFICIENT; }));
+ buttonON = true;
+ RaiseBackButtonPressed?.Invoke(buttonON);
+ }
+ else
+ {
+ layout.Children.Remove(FinishButton);
+ buttonON = false;
+ RaiseBackButtonPressed?.Invoke(buttonON);
+ }
+ return true;
+ }
+
+ public void Rotate(RotaryEventArgs args)
+ {
+ EventHandler<RotaryEventArgs> handler = RaiseRotaryEvent;
+ if (handler != null)
+ {
+ handler(this, args);
+ }
+ return;
+ }
+
+ public void BackgroundBottomFade(bool on)
+ {
+ if (on == true) BackgroundBottomLight.FadeTo(FULL_OPACITY, BACKGROUND_FADE_TIME);
+ else BackgroundBottomLight.FadeTo(ZERO_OPACITY, BACKGROUND_FADE_TIME);
+ }
+
+ public void BackgroundWholeFade(bool on)
+ {
+ if (on == true) BackgroundWholeLight.FadeTo(FULL_OPACITY, BACKGROUND_FADE_TIME);
+ else BackgroundWholeLight.FadeTo(ZERO_OPACITY, BACKGROUND_FADE_TIME);
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8" ?>
+<c:CirclePage
+ xmlns="http://xamarin.com/schemas/2014/forms"
+ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+ xmlns:c="clr-namespace:Tizen.Wearable.CircularUI.Forms;assembly=XSF"
+ x:Class="MapMyRun.Tizen.Views.WorkoutSetupPage"
+ NavigationPage.HasNavigationBar="False"
+ RotaryFocusObject="{x:Reference workoutSetupList}">
+ <c:CirclePage.Content>
+ <c:CircleListView x:Name="workoutSetupList" ItemTapped="workoutSetupList_ItemTapped" Header="Workout Setup">
+ <c:CircleListView.ItemTemplate>
+ <DataTemplate>
+ <ViewCell>
+ <StackLayout>
+ <Label
+ Text="{Binding Name}"
+ Style="{StaticResource PrimaryCellLabelStyle}"
+ />
+ <Label
+ Text="{Binding SubText}"
+ IsVisible="{Binding HasSubText}"
+ Style="{StaticResource DetailCellLabelStyle}"
+ />
+ </StackLayout>
+ </ViewCell>
+ </DataTemplate>
+ </c:CircleListView.ItemTemplate>
+ <c:CircleListView.HeaderTemplate>
+ <DataTemplate>
+ <Label
+ c:CircleListView.CancelEffect="True"
+ HeightRequest="120"
+ Text="{Binding .}"
+ Style="{StaticResource PrimaryHeaderStyle}"/>
+ </DataTemplate>
+ </c:CircleListView.HeaderTemplate>
+ </c:CircleListView>
+ </c:CirclePage.Content>
+</c:CirclePage>
--- /dev/null
+using MapMyRun.Tizen.ViewModels;
+using MapMyRun.Tizen.Views.Templates;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Tizen.Wearable.CircularUI.Forms;
+using Xamarin.Forms;
+using Xamarin.Forms.Xaml;
+
+namespace MapMyRun.Tizen.Views
+{
+ [XamlCompilation(XamlCompilationOptions.Compile)]
+ public partial class WorkoutSetupPage : CirclePage
+ {
+ private WorkoutSetupPageModel setupPageModel;
+
+ private ObservableCollection<SettingCell> workoutSetup;
+
+ private SettingCell currentActivity;
+
+ public WorkoutSetupPage(WorkoutSetupPageModel _setupPageModel)
+ {
+ InitializeComponent();
+
+ setupPageModel = _setupPageModel;
+ BindingContext = setupPageModel;
+
+ workoutSetup = new ObservableCollection<SettingCell>();
+
+ currentActivity = new SettingCell("Select Activity", setupPageModel.currentActivity);
+ workoutSetup.Add(currentActivity);
+ workoutSetup.Add(new SettingCell("Goal", "Basic Workout"));
+
+ workoutSetupList.ItemsSource = workoutSetup;
+ }
+
+ protected override void OnAppearing()
+ {
+ base.OnAppearing();
+
+ currentActivity.SubText = setupPageModel.currentActivity;
+ }
+
+ async private void workoutSetupList_ItemTapped(object sender, ItemTappedEventArgs e)
+ {
+ string tappedItem = workoutSetup[e.ItemIndex].Name;
+
+ switch (tappedItem)
+ {
+ case "Select Activity":
+ await Navigation.PushAsync(new SelectWorkoutPage(setupPageModel));
+ break;
+ case "Goal":
+ await DisplayAlert($"{tappedItem}", "Not Implemented", "OK");
+ break;
+ default:
+ break;
+ }
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.29609.76
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MapMyRun.Tizen", "MapMyRun.Tizen\MapMyRun.Tizen.csproj", "{9645B7A1-AFE7-4150-BE16-278AB8C01A93}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MapMyRun.Tests", "MapMyRun.Tests\MapMyRun.Tests.csproj", "{C23F236F-2FFE-437C-85C5-11329CE83834}"
+EndProject
+Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "MapMyRun", "MapMyRun\MapMyRun.shproj", "{3C82C042-F1F4-42E3-B82D-DFED5E06EE19}"
+EndProject
+Global
+ GlobalSection(SharedMSBuildProjectFiles) = preSolution
+ MapMyRun\MapMyRun.projitems*{3c82c042-f1f4-42e3-b82d-dfed5e06ee19}*SharedItemsImports = 13
+ EndGlobalSection
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {9645B7A1-AFE7-4150-BE16-278AB8C01A93}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9645B7A1-AFE7-4150-BE16-278AB8C01A93}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9645B7A1-AFE7-4150-BE16-278AB8C01A93}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9645B7A1-AFE7-4150-BE16-278AB8C01A93}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C23F236F-2FFE-437C-85C5-11329CE83834}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C23F236F-2FFE-437C-85C5-11329CE83834}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C23F236F-2FFE-437C-85C5-11329CE83834}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C23F236F-2FFE-437C-85C5-11329CE83834}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {E7AF0B79-CC16-49A2-B49D-119F09B9D5A7}
+ EndGlobalSection
+EndGlobal
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <PropertyGroup>
+ <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
+ <HasSharedItems>true</HasSharedItems>
+ <SharedGUID>3c82c042-f1f4-42e3-b82d-dfed5e06ee19</SharedGUID>
+ </PropertyGroup>
+ <PropertyGroup Label="Configuration">
+ <Import_RootNamespace>MapMyRun</Import_RootNamespace>
+ </PropertyGroup>
+ <ItemGroup>
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Activity.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\ActivityRepository.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Dispatcher.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\IAppLauncher.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\IDispatcher.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\IMessagePortHandler.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\IPermissionManager.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Workout\Items\AverageCadence.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Workout\Items\ZoneHr.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Workout\Items\AveragePace.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Workout\Items\CurrentCadence.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Workout\Items\CurrentPace.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Workout\Items\Distance.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Workout\Items\Duration.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Workout\Items\MaxPace.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Workout\Items\MaxSpeed.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Workout\Items\AverageSpeed.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Workout\Items\CurrentSpeed.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Settings\ISettingManager.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Settings\SettingManager.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Settings\SettingsData.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Workout\Items\Calories.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Workout\WorkoutData.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Workout\Items\PeakHr.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Workout\Items\CurrentHr.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Workout\Items\AverageHr.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Workout\Items\IWorkoutItem.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\PermissionManager.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\IPhoneService.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\IPlatformService.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Loading.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\NotificationProvider.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\NotificationObserver.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\PhoneService.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Request.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Response.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Workout\Workout.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Workout\Items\WorkoutItemFactory.cs" />
+ </ItemGroup>
+</Project>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <PropertyGroup Label="Globals">
+ <ProjectGuid>3c82c042-f1f4-42e3-b82d-dfed5e06ee19</ProjectGuid>
+ <MinimumVisualStudioVersion>14.0</MinimumVisualStudioVersion>
+ </PropertyGroup>
+ <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+ <Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props" />
+ <Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props" />
+ <PropertyGroup />
+ <Import Project="MapMyRun.projitems" Label="Shared" />
+ <Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" />
+</Project>
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRun.Models
+{
+ public class Activity
+ {
+ public int id { get; set; } = -1;
+
+ public int group_id { get; set; } = -1;
+
+ public bool useGPS { get; set; } = false;
+
+ public bool useDistance { get; set; } = false;
+
+ public bool usePace { get; set; } = false;
+
+ public bool shoesSupported { get; set; } = false;
+
+ public bool cadenceSupported { get; set; } = false;
+
+ public bool hasSteps { get; set; } = false;
+
+ // 0 - unsupported, 1 - Cadence, 2 - Stride, while yet unable to handle enum
+ public int coachingSupported { get; set; } = 0;
+
+ public string name { get; set; } = "undefined";
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace MapMyRun.Models
+{
+ public class ActivityRepository
+ {
+ // Activities data taken from mmr.activities.ts and activity.ts
+ private const string activityListJson = @"[
+ { id: 1, group_id: 3, useGPS: false, useDistance: false, usePace: false },
+ { id: 7, group_id: 6, useGPS: true, useDistance: true, usePace: true, shoesSupported: true, cadenceSupported: true, coachingSupported: 1 },
+ { id: 9, group_id: 9, useGPS: true, useDistance: true, usePace: true, shoesSupported: true, cadenceSupported: true, hasSteps: true },
+ { id: 10, group_id: 10, useGPS: true, useDistance: true, usePace: false },
+ { id: 16, group_id: 6, useGPS: true, useDistance: true, usePace: true, shoesSupported: true, cadenceSupported: true, coachingSupported: 1 },
+ { id: 19, group_id: 1, useGPS: false, useDistance: false, usePace: false },
+ { id: 20, group_id: 8, useGPS: false, useDistance: true, usePace: false },
+ { id: 21, group_id: 7, useGPS: true, useDistance: true, usePace: false },
+ { id: 22, group_id: 5, useGPS: false, useDistance: false, usePace: false },
+ { id: 23, group_id: 0, useGPS: false, useDistance: false, usePace: false },
+ { id: 24, group_id: 9, useGPS: true, useDistance: true, usePace: true, shoesSupported: true, cadenceSupported: true, hasSteps: true },
+ { id: 26, group_id: 3, useGPS: false, useDistance: false, usePace: false },
+ { id: 28, group_id: 3, useGPS: false, useDistance: false, usePace: false },
+ { id: 29, group_id: 7, useGPS: true, useDistance: true, usePace: false },
+ { id: 31, group_id: 0, useGPS: false, useDistance: false, usePace: false },
+ { id: 33, group_id: 1, useGPS: true, useDistance: true, usePace: false },
+ { id: 34, group_id: 0, useGPS: false, useDistance: false, usePace: false },
+ { id: 36, group_id: 1, useGPS: true, useDistance: true, usePace: false },
+ { id: 41, group_id: 1, useGPS: true, useDistance: true, usePace: false },
+ { id: 43, group_id: 5, useGPS: true, useDistance: true, usePace: false },
+ { id: 44, group_id: 1, useGPS: true, useDistance: true, usePace: false },
+ { id: 45, group_id: 6, useGPS: true, useDistance: true, usePace: false, shoesSupported: true, cadenceSupported: true },
+ { id: 45, group_id: 1, useGPS: true, useDistance: true, usePace: false },
+ { id: 45, group_id: 8, useGPS: true, useDistance: true, usePace: false },
+ { id: 47, group_id: 1, useGPS: true, useDistance: true, usePace: false },
+ { id: 54, group_id: 7, useGPS: false, useDistance: false, usePace: false },
+ { id: 56, group_id: 1, useGPS: true, useDistance: true, usePace: false },
+ { id: 57, group_id: 5, useGPS: true, useDistance: true, usePace: false },
+ { id: 59, group_id: 8, useGPS: false, useDistance: false, usePace: false },
+ { id: 60, group_id: 1, useGPS: true, useDistance: true, usePace: false },
+ { id: 65, group_id: 7, useGPS: false, useDistance: false, usePace: false },
+ { id: 68, group_id: 7, useGPS: true, useDistance: false, usePace: false },
+ { id: 71, group_id: 6, useGPS: false, useDistance: true, usePace: true, shoesSupported: true, cadenceSupported: true, coachingSupported: 1 },
+ { id: 72, group_id: 4, useGPS: false, useDistance: false, usePace: false },
+ { id: 73, group_id: 7, useGPS: true, useDistance: true, usePace: false },
+ { id: 74, group_id: 10, useGPS: true, useDistance: true, usePace: false },
+ { id: 75, group_id: 8, useGPS: false, useDistance: false, usePace: false },
+ { id: 77, group_id: 5, useGPS: false, useDistance: false, usePace: false },
+ { id: 80, group_id: 5, useGPS: true, useDistance: true, usePace: false },
+ { id: 82, group_id: 2, useGPS: false, useDistance: false, usePace: false },
+ { id: 84, group_id: 7, useGPS: true, useDistance: true, usePace: false },
+ { id: 85, group_id: 5, useGPS: true, useDistance: true, usePace: false },
+ { id: 86, group_id: 7, useGPS: true, useDistance: true, usePace: false },
+ { id: 94, group_id: 7, useGPS: true, useDistance: true, usePace: false },
+ { id: 95, group_id: 7, useGPS: true, useDistance: true, usePace: false },
+ { id: 96, group_id: 5, useGPS: false, useDistance: false, usePace: false },
+ { id: 99, group_id: 3, useGPS: false, useDistance: false, usePace: false },
+ { id: 101, group_id: 5, useGPS: true, useDistance: true, usePace: false },
+ { id: 102, group_id: 6, useGPS: true, useDistance: true, usePace: true, shoesSupported: true, cadenceSupported: true, coachingSupported: 1 },
+ { id: 103, group_id: 6, useGPS: true, useDistance: true, usePace: true, shoesSupported: true, cadenceSupported: true },
+ { id: 104, group_id: 7, useGPS: false, useDistance: false, usePace: false },
+ { id: 105, group_id: 9, useGPS: true, useDistance: true, usePace: true, shoesSupported: true, cadenceSupported: true, hasSteps: true },
+ { id: 107, group_id: 10, useGPS: true, useDistance: true, usePace: false },
+ { id: 108, group_id: 6, useGPS: true, useDistance: true, usePace: true, shoesSupported: true, cadenceSupported: true, coachingSupported: 1 },
+ { id: 111, group_id: 6, useGPS: false, useDistance: true, usePace: true, shoesSupported: true, cadenceSupported: true },
+ { id: 115, group_id: 1, useGPS: true, useDistance: true, usePace: false },
+ { id: 116, group_id: 7, useGPS: false, useDistance: false, usePace: false },
+ { id: 119, group_id: 10, useGPS: true, useDistance: true, usePace: true },
+ { id: 120, group_id: 0, useGPS: false, useDistance: false, usePace: false },
+ { id: 121, group_id: 8, useGPS: false, useDistance: false, usePace: false },
+ { id: 123, group_id: 0, useGPS: false, useDistance: false, usePace: false },
+ { id: 124, group_id: 6, useGPS: true, useDistance: true, usePace: true, shoesSupported: true, cadenceSupported: true, coachingSupported: 1 },
+ { id: 127, group_id: 7, useGPS: true, useDistance: true, usePace: false },
+ { id: 128, group_id: 7, useGPS: true, useDistance: true, usePace: false },
+ { id: 131, group_id: 7, useGPS: false, useDistance: false, usePace: false },
+ { id: 133, group_id: 9, useGPS: true, useDistance: true, usePace: false, shoesSupported: true, cadenceSupported: true, hasSteps: true },
+ { id: 134, group_id: 7, useGPS: true, useDistance: true, usePace: false },
+ { id: 135, group_id: 7, useGPS: false, useDistance: false, usePace: false },
+ { id: 137, group_id: 5, useGPS: true, useDistance: true, usePace: false },
+ { id: 138, group_id: 7, useGPS: true, useDistance: true, usePace: false },
+ { id: 141, group_id: 7, useGPS: false, useDistance: false, usePace: false },
+ { id: 142, group_id: 7, useGPS: false, useDistance: false, usePace: false },
+ { id: 148, group_id: 7, useGPS: true, useDistance: true, usePace: false },
+ { id: 153, group_id: 7, useGPS: true, useDistance: true, usePace: false },
+ { id: 154, group_id: 7, useGPS: true, useDistance: true, usePace: true },
+ { id: 155, group_id: 7, useGPS: false, useDistance: false, usePace: false },
+ { id: 156, group_id: 7, useGPS: true, useDistance: true, usePace: false },
+ { id: 161, group_id: 7, useGPS: true, useDistance: true, usePace: false },
+ { id: 163, group_id: 7, useGPS: false, useDistance: false, usePace: false },
+ { id: 164, group_id: 0, useGPS: false, useDistance: false, usePace: false },
+ { id: 169, group_id: 5, useGPS: true, useDistance: true, usePace: false },
+ { id: 171, group_id: 5, useGPS: true, useDistance: true, usePace: false },
+ { id: 172, group_id: 6, useGPS: true, useDistance: true, usePace: true, shoesSupported: true, cadenceSupported: true, coachingSupported: 1 },
+ { id: 176, group_id: 7, useGPS: true, useDistance: true, usePace: false },
+ { id: 178, group_id: 7, useGPS: true, useDistance: true, usePace: false },
+ { id: 180, group_id: 8, useGPS: true, useDistance: true, usePace: false },
+ { id: 182, group_id: 5, useGPS: true, useDistance: true, usePace: false },
+ { id: 184, group_id: 5, useGPS: true, useDistance: true, usePace: false },
+ { id: 192, group_id: 8, useGPS: false, useDistance: false, usePace: false },
+ { id: 193, group_id: 8, useGPS: false, useDistance: false, usePace: false },
+ { id: 197, group_id: 6, useGPS: true, useDistance: true, usePace: true, shoesSupported: true, cadenceSupported: true, coachingSupported: 1 },
+ { id: 199, group_id: 3, useGPS: false, useDistance: false, usePace: false },
+ { id: 200, group_id: 5, useGPS: true, useDistance: true, usePace: false },
+ { id: 201, group_id: 6, useGPS: true, useDistance: true, usePace: true, shoesSupported: true, cadenceSupported: true },
+ { id: 204, group_id: 9, useGPS: true, useDistance: true, usePace: true, shoesSupported: true, cadenceSupported: true, hasSteps: true },
+ { id: 205, group_id: 7, useGPS: true, useDistance: true, usePace: false },
+ { id: 208, group_id: 3, useGPS: false, useDistance: true, usePace: true, shoesSupported: true, cadenceSupported: true, coachingSupported: 1 },
+ { id: 208, group_id: 6, useGPS: false, useDistance: true, usePace: true, shoesSupported: true, cadenceSupported: true, coachingSupported: 1 },
+ { id: 214, group_id: 5, useGPS: false, useDistance: false, usePace: false },
+ { id: 216, group_id: 7, useGPS: false, useDistance: false, usePace: false },
+ { id: 221, group_id: 5, useGPS: true, useDistance: true, usePace: false },
+ { id: 222, group_id: 7, useGPS: false, useDistance: false, usePace: false },
+ { id: 224, group_id: 5, useGPS: true, useDistance: true, usePace: false },
+ { id: 227, group_id: 6, useGPS: true, useDistance: true, usePace: true, shoesSupported: true, cadenceSupported: true },
+ { id: 228, group_id: 7, useGPS: false, useDistance: false, usePace: false },
+ { id: 230, group_id: 0, useGPS: false, useDistance: false, usePace: false },
+ { id: 235, group_id: 7, useGPS: true, useDistance: true, usePace: false },
+ { id: 250, group_id: 6, useGPS: false, useDistance: false, usePace: false, shoesSupported: true, cadenceSupported: true },
+ { id: 251, group_id: 7, useGPS: false, useDistance: false, usePace: false },
+ { id: 254, group_id: 7, useGPS: false, useDistance: false, usePace: false },
+ { id: 257, group_id: 5, useGPS: true, useDistance: true, usePace: false },
+ { id: 258, group_id: 7, useGPS: false, useDistance: false, usePace: false },
+ { id: 259, group_id: 7, useGPS: false, useDistance: false, usePace: false },
+ { id: 263, group_id: 3, useGPS: false, useDistance: false, usePace: false },
+ { id: 264, group_id: 7, useGPS: true, useDistance: true, usePace: true },
+ { id: 266, group_id: 6, useGPS: true, useDistance: true, usePace: true, shoesSupported: true, cadenceSupported: true },
+ { id: 267, group_id: 5, useGPS: false, useDistance: false, usePace: false },
+ { id: 271, group_id: 7, useGPS: true, useDistance: true, usePace: false },
+ { id: 275, group_id: 9, useGPS: true, useDistance: true, usePace: true, shoesSupported: true, cadenceSupported: true, hasSteps: true },
+ { id: 284, group_id: 8, useGPS: false, useDistance: false, usePace: false },
+ { id: 336, group_id: 7, useGPS: true, useDistance: true, usePace: false },
+ { id: 468, group_id: 4, useGPS: false, useDistance: false, usePace: false },
+ { id: 469, group_id: 4, useGPS: false, useDistance: false, usePace: false },
+ { id: 471, group_id: 4, useGPS: false, useDistance: false, usePace: false },
+ { id: 472, group_id: 7, useGPS: false, useDistance: false, usePace: false },
+ { id: 473, group_id: 4, useGPS: false, useDistance: false, usePace: false },
+ { id: 498, group_id: 11, useGPS: false, useDistance: false, usePace: false },
+ { id: 499, group_id: 11, useGPS: false, useDistance: false, usePace: false },
+ { id: 500, group_id: 11, useGPS: false, useDistance: false, usePace: false },
+ { id: 546, group_id: 1, useGPS: false, useDistance: false, usePace: false },
+ { id: 548, group_id: 3, useGPS: false, useDistance: false, usePace: false },
+ { id: 564, group_id: 4, useGPS: false, useDistance: false, usePace: false },
+ { id: 598, group_id: 3, useGPS: false, useDistance: false, usePace: false },
+ { id: 622, group_id: 2, useGPS: false, useDistance: false, usePace: false },
+ { id: 627, group_id: 0, useGPS: false, useDistance: false, usePace: false },
+ { id: 633, group_id: 1, useGPS: true, useDistance: true, usePace: false },
+ { id: 637, group_id: 1, useGPS: true, useDistance: true, usePace: false },
+ { id: 704, group_id: 0, useGPS: false, useDistance: false, usePace: false },
+ { id: 708, group_id: 7, useGPS: true, useDistance: true, usePace: false },
+ { id: 710, group_id: 6, useGPS: true, useDistance: true, usePace: false, shoesSupported: true, cadenceSupported: true },
+ { id: 714, group_id: 7, useGPS: true, useDistance: true, usePace: false },
+ { id: 722, group_id: 11, useGPS: false, useDistance: false, usePace: false },
+ { id: 724, group_id: 11, useGPS: false, useDistance: false, usePace: false },
+ { id: 726, group_id: 11, useGPS: false, useDistance: false, usePace: false },
+ { id: 728, group_id: 3, useGPS: false, useDistance: false, usePace: false },
+ { id: 730, group_id: 3, useGPS: false, useDistance: false, usePace: false },
+ { id: 732, group_id: 5, useGPS: false, useDistance: false, usePace: false },
+ { id: 740, group_id: 4, useGPS: false, useDistance: false, usePace: false },
+ { id: 742, group_id: 2, useGPS: false, useDistance: false, usePace: false },
+ { id: 746, group_id: 2, useGPS: false, useDistance: false, usePace: false },
+ { id: 748, group_id: 2, useGPS: false, useDistance: false, usePace: false },
+ { id: 750, group_id: 2, useGPS: false, useDistance: false, usePace: false },
+ { id: 752, group_id: 2, useGPS: false, useDistance: false, usePace: false },
+ { id: 754, group_id: 0, useGPS: false, useDistance: false, usePace: false },
+ { id: 756, group_id: 6, useGPS: false, useDistance: true, usePace: true, shoesSupported: true, cadenceSupported: true, coachingSupported: 1 },
+ { id: 758, group_id: 7, useGPS: false, useDistance: false, usePace: false },
+ { id: 760, group_id: 4, useGPS: false, useDistance: false, usePace: false },
+ { id: 762, group_id: 4, useGPS: false, useDistance: false, usePace: false },
+ { id: 764, group_id: 4, useGPS: false, useDistance: false, usePace: false },
+ { id: 768, group_id: 7, useGPS: true, useDistance: true, usePace: false },
+ { id: 813, group_id: 4, useGPS: false, useDistance: false, usePace: false },
+ { id: 819, group_id: 7, useGPS: false, useDistance: false, usePace: false },
+ { id: 825, group_id: 9, useGPS: true, useDistance: true, usePace: true, shoesSupported: true, cadenceSupported: true, hasSteps: true },
+ { id: 827, group_id: 9, useGPS: true, useDistance: true, usePace: true, shoesSupported: true, cadenceSupported: true, hasSteps: true },
+ { id: 829, group_id: 6, useGPS: true, useDistance: true, usePace: true, shoesSupported: true, cadenceSupported: true },
+ { id: 831, group_id: 6, useGPS: true, useDistance: true, usePace: true, shoesSupported: true, cadenceSupported: true },
+ { id: 845, group_id: 5, useGPS: false, useDistance: false, usePace: false },
+ { id: 855, group_id: 6, useGPS: true, useDistance: true, usePace: true, shoesSupported: true, cadenceSupported: true },
+ { id: 861, group_id: 3, useGPS: false, useDistance: false, usePace: false },
+ { id: 863, group_id: 5, useGPS: true, useDistance: true, usePace: false },
+ { id: 880, group_id: 4, useGPS: false, useDistance: false, usePace: false },
+ { id: 882, group_id: 0, useGPS: false, useDistance: false, usePace: false },
+ { id: 890, group_id: 3, useGPS: false, useDistance: false, usePace: false }
+ ]";
+
+ public List<Activity> activityGroupings { get; private set; }
+
+ public List<Activity> completeActivityList { get; private set; }
+
+ public List<Activity> popularActivityList { get; private set; }
+
+ private List<Activity> activityListByGroup { get; set; }
+
+ public List<Activity> GetActivitiesByGroupId(int group_id)
+ {
+ activityListByGroup = new List<Activity>();
+ activityListByGroup = completeActivityList.FindAll((Activity activity) => { return activity.group_id == group_id; });
+
+ return activityListByGroup;
+ }
+
+ private void GeneratePopularActivityList()
+ {
+ popularActivityList = new List<Activity>();
+
+ List<int> popularActivityIds = new List<int> { 16, 266, 208, 829, 197, 9, 204, 24, 36, 41 };
+ popularActivityIds.ForEach((int id) => { popularActivityList.Add(GetActivityById(id)); });
+ }
+
+ public ActivityRepository(string localizationJson)
+ {
+ completeActivityList = JsonConvert.DeserializeObject<List<Activity>>(activityListJson);
+ UpdateNames(localizationJson);
+ DefineGroups();
+ GeneratePopularActivityList();
+ }
+
+ public Activity GetActivityById(int id)
+ {
+ try
+ {
+ return completeActivityList.First((Activity activity) => activity.id == id);
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine($"Exception at finding activity with id {id}. Check if activity exists.");
+ Console.WriteLine(e.StackTrace);
+
+ return null;
+ }
+ }
+
+ private void DefineGroups()
+ {
+ activityGroupings = new List<Activity>();
+
+ activityGroupings.Add(new Activity() { group_id = 0, name = "Class" });
+ activityGroupings.Add(new Activity() { group_id = 1, name = "Cycling" });
+ activityGroupings.Add(new Activity() { group_id = 2, name = "Dance" });
+ activityGroupings.Add(new Activity() { group_id = 3, name = "Gym" });
+ activityGroupings.Add(new Activity() { group_id = 4, name = "Martial Arts" });
+ activityGroupings.Add(new Activity() { group_id = 6, name = "Run" });
+ activityGroupings.Add(new Activity() { group_id = 7, name = "Sport" });
+ activityGroupings.Add(new Activity() { group_id = 8, name = "Swim" });
+ activityGroupings.Add(new Activity() { group_id = 9, name = "Walk" });
+ activityGroupings.Add(new Activity() { group_id = 10, name = "Winter" });
+ activityGroupings.Add(new Activity() { group_id = 11, name = "Yoga" });
+ activityGroupings.Add(new Activity() { group_id = 5, name = "Other" });
+ }
+
+ // NOTE: Implementation regarding retrieval of names to be updated when proper localization implementation is done
+ private void UpdateNames(string localizationJson)
+ {
+ JObject localizedStrings = JObject.Parse(localizationJson);
+
+ const string activityKey = "activity_id";
+
+ completeActivityList.ForEach((Activity activity) => {
+ activity.name = localizedStrings[$"{activityKey}_{activity.id}"]?.ToString();
+ });
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace MapMyRun.Models
+{
+ public class Dispatcher : IDispatcher
+ {
+ IMessagePortHandler _messagePortHandler = null;
+ private const string ServiceAppId = "org.tizen.example.MapMyRunService";
+ //private const string ServiceAppId = "org.tizen.mapmyrunservice";
+ private const string LocalPort = "APP_PORT";
+ private const string RemotePort = "SERVICE_PORT";
+ public const string OperationKey = "operation";
+ public const string DataKey = "data";
+ public const string TransIdKey = "transID";
+ public const string ResponseStatusKey = "status";
+ private const bool SecureMode = false;
+ private uint TransIdIndex = 0;
+ private readonly Dictionary<string, Request> RequestCache = new Dictionary<string, Request>();
+ private readonly List<NotificationProvider> NotificationProviders = new List<NotificationProvider>();
+
+ public static Dispatcher Instance { get; private set; } = new Dispatcher();
+
+ private Dispatcher()
+ {
+ }
+
+ public void Initialize(IMessagePortHandler messagePortHandler)
+ {
+ _messagePortHandler = messagePortHandler;
+ _messagePortHandler.MessageReceived += OnReceiveMessage;
+ _messagePortHandler.Connect(LocalPort, ServiceAppId, RemotePort, SecureMode);
+ }
+
+ public string SendRequest(Request request)
+ {
+ request.TransID = TransIdIndex.ToString();
+ TransIdIndex++;
+ RequestCache.Add(request.TransID, request);
+ var dataSet = new Dictionary<string, string>
+ {
+ { OperationKey, ((int)request.Operation).ToString() },
+ { DataKey, request.Data },
+ { TransIdKey, request.TransID }
+ };
+
+ _messagePortHandler.Send(dataSet);
+ return request.TransID;
+ }
+
+ public void OnReceiveMessage(Dictionary<string, string> dataSet)
+ {
+ if (dataSet.TryGetValue(OperationKey, out string operation))
+ {
+ Console.WriteLine($"Receive Notification: {operation}");
+
+ try
+ {
+ OperationType type = (OperationType)Int32.Parse(operation);
+ if (dataSet.TryGetValue(DataKey, out string data))
+ PostNotification(data, type);
+ }
+ catch
+ {
+ Console.WriteLine("Invalid Operation");
+ }
+ return;
+ }
+
+ if (!dataSet.TryGetValue(TransIdKey, out string tid))
+ {
+ Console.WriteLine("Request for operation response is not found");
+ return;
+ }
+
+ if (RequestCache.TryGetValue(tid, out Request cachedRequest))
+ {
+ dataSet.TryGetValue(DataKey, out string data);
+ dataSet.TryGetValue(ResponseStatusKey, out string status);
+
+ if (!Enum.TryParse(status, out ResponseStatus responseStatus))
+ {
+ responseStatus = ResponseStatus.Fail;
+ }
+
+ var response = new Response(tid, data, responseStatus);
+ cachedRequest.OnReceiveResponse(response);
+ }
+ }
+
+ void PostNotification(string data, OperationType type)
+ {
+ NotificationProvider notificationMonitor;
+ try
+ {
+ notificationMonitor = NotificationProviders.Find(x => x != null && x.Type == type);
+ notificationMonitor.PostNotification(data);
+ }
+ catch
+ {
+ Console.WriteLine($"Notification Monitor none: {type}");
+ }
+ }
+
+ public NotificationProvider Listen(OperationType type)
+ {
+ NotificationProvider notificationMonitor;
+ try
+ {
+ notificationMonitor = NotificationProviders.Find(x => x != null && x.Type == type);
+ Console.WriteLine($"Listen exist Notification Monitor {notificationMonitor.Type}");
+ return notificationMonitor;
+ }
+ catch
+ {
+ notificationMonitor = new NotificationProvider(type);
+ NotificationProviders.Add(notificationMonitor);
+ Console.WriteLine($"Create new Notification Monitor {notificationMonitor.Type}");
+ return notificationMonitor;
+ }
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRun.Models
+{
+ public interface IAppLauncher
+ {
+ void Launch(string appId);
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace MapMyRun.Models
+{
+ public interface IDispatcher
+ {
+ void Initialize(IMessagePortHandler messagePortHandler);
+
+ string SendRequest(Request request);
+
+ void OnReceiveMessage(Dictionary<string, string> data);
+
+ NotificationProvider Listen(OperationType type);
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace MapMyRun.Models
+{
+ public interface IMessagePortHandler
+ {
+ void Connect(string localPort, string remoteAppId, string remotePort, bool secureMode);
+
+ void Disconnect();
+
+ void Send(Dictionary<string, string> data);
+
+ event Action<Dictionary<string, string>> MessageReceived;
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRun.Models
+{
+ public enum PermissionType
+ {
+ Location,
+ Sensor
+ }
+
+ public delegate void OnPermissionResponse(bool isSuccess, bool isGranted);
+
+ public interface IPermissionManager
+ {
+ bool HasPermission(PermissionType permission);
+
+ void RequestPermission(PermissionType permission, OnPermissionResponse onResponse);
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRun.Models
+{
+ public interface IPhoneService
+ {
+ bool IsPlatformSwVersionSupported();
+
+ bool IsAuthenticated(Action<PhoneApplicationState> authenticatedCb);
+
+ void InitializeApp();
+
+ void KeepScreenOn();
+
+ void SetAppTerminateTimer(int sec);
+
+ void ClearAppTerminateTimer();
+
+ void OnAppStart();
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRun.Models
+{
+ public interface IPlatformService
+ {
+ void GetSwVersion(out string version);
+
+ void KeepScreenOn();
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using MapMyRun.Models.Settings;
+
+namespace MapMyRun.Models
+{
+ public enum LoadingState
+ {
+ Not_Started = 1,
+ Connect= 2,
+ Install = 3,
+ Auth = 4,
+ Initializers = 5,
+ Finished = 6,
+ }
+
+ public class Loading
+ {
+ LoadingState _state = LoadingState.Not_Started;
+ readonly string _serviceAppId ;
+ readonly IAppLauncher _appLauncher;
+ readonly IDispatcher _dispatcher;
+ readonly IPhoneService _phoneService;
+ readonly IMessagePortHandler _messagePortHandler;
+ readonly ISettingManager _settingManager;
+ NotificationObserver _logoutObserver;
+ public LoadingState State
+ {
+ get { return _state; }
+ set
+ {
+ _state = value;
+ LoadingStateChanged?.Invoke(_state);
+ }
+ }
+
+ public event Action<LoadingState> LoadingStateChanged;
+ public PhoneApplicationState phoneState = PhoneApplicationState.NotSupportedTizenVersion;
+
+ public Loading(IAppLauncher appLauncher, IMessagePortHandler messagePortHandler,
+ IDispatcher dispatcher, IPhoneService phoneService, ISettingManager settingManager, string serviceAppId)
+ {
+ _appLauncher = appLauncher;
+ _serviceAppId = serviceAppId;
+ _dispatcher = dispatcher;
+ _messagePortHandler = messagePortHandler;
+ _phoneService = phoneService;
+ _settingManager = settingManager;
+ }
+
+ public void StartLoading()
+ {
+ if (!_phoneService.IsPlatformSwVersionSupported())
+ return;
+
+ _phoneService.KeepScreenOn();
+
+ State = LoadingState.Connect;
+ if (!LaunchServiceApp())
+ return;
+
+ ConnectServiceApp();
+
+ //TODO : Move to converter
+ GetUserProfileForDisplayMeasurementSystem();
+
+ State = LoadingState.Auth;
+
+ _phoneService.IsAuthenticated((state) =>
+ {
+ _phoneService.InitializeApp();
+ UpdatePhoneState(state);
+ });
+ }
+
+ private void UpdatePhoneState(PhoneApplicationState state)
+ {
+ phoneState = state;
+ switch (state)
+ {
+ case PhoneApplicationState.NoInternetConnection:
+ case PhoneApplicationState.NoBTE:
+ case PhoneApplicationState.Timeout:
+ case PhoneApplicationState.NotInstalled:
+ case PhoneApplicationState.NotLoggedIn:
+ case PhoneApplicationState.NotSupportedTizenVersion:
+ //this._isLoadingError = true;
+ break;
+ case PhoneApplicationState.LoggedIn:
+ case PhoneApplicationState.Authenticated:
+ StartInitialization();
+ break;
+ }
+ }
+
+ private void StartInitialization()
+ {
+ State = LoadingState.Initializers;
+
+ InitializeLogoutEventHandler();
+
+ InitializeUserData();
+
+ InitializeConsent();
+
+ InitializeUserGear();
+
+ InitializeSettings();
+
+ State = LoadingState.Finished;
+
+ InitializeAfterLoading();
+ }
+
+ private void GetUserProfileForDisplayMeasurementSystem()
+ {
+ var request = new Request(OperationType.GetData, "{ \"action\":" + (int)DataActionType.GetUserProfile + " }");
+
+ request.ResponseReceived += (Response response) =>
+ {
+ //TODO : Measurement Converter
+ Console.WriteLine($"Get User Profile for Measurement System");
+ };
+ _dispatcher.SendRequest(request);
+ }
+
+ private void SendConsentOkReturn()
+ {
+ var request = new Request(OperationType.GetConsentStatus, "{ \"isUpToDate\": true }");
+
+ request.ResponseReceived += (Response response) =>
+ {
+ Console.WriteLine($"Get Consent Status");
+ };
+ _dispatcher.SendRequest(request);
+ }
+
+ private void SendGearStatusOkReturn()
+ {
+ var request = new Request(OperationType.GetUserGear, "{ \"isUpToDate\": true }");
+
+ request.ResponseReceived += (Response response) =>
+ {
+ Console.WriteLine($"Get Consent Status");
+ };
+ _dispatcher.SendRequest(request);
+ }
+
+ public void SendTestMessage()
+ {
+ var startRequest = new Request(OperationType.ApplicationInitialized, "");
+
+ startRequest.ResponseReceived += (Response response) =>
+ {
+ // TODO : Implement Cache Storage
+ //if (result && result.data)
+ //{
+ // this.storageService.save(DEVICE_TYPE_STORAGE_KEY, result.data.deviceType);
+ //}
+ Console.WriteLine($"PhoneService: Initialize app request result");
+ };
+ _dispatcher.SendRequest(startRequest);
+ }
+
+ private bool LaunchServiceApp()
+ {
+ Console.WriteLine($"Launching service app! {_serviceAppId}");
+ try
+ {
+ _appLauncher.Launch(_serviceAppId);
+ }
+ catch (Exception e)
+ {
+ //TODO : Handle Fail Case
+ Console.WriteLine($"Unable to launch app {_serviceAppId }");
+ Console.WriteLine(e.StackTrace);
+ return false;
+ }
+
+ Console.WriteLine($"Launching service app SUCCESS!");
+ return true;
+ }
+
+ private void ConnectServiceApp()
+ {
+ Console.WriteLine($"Connect to service app");
+ try
+ {
+ _dispatcher.Initialize(_messagePortHandler);
+ }
+ catch (Exception e)
+ {
+ //TODO : Handle Fail Case
+ Console.WriteLine(e.StackTrace);
+ }
+ State = LoadingState.Install;
+ }
+
+ public void InitializeLogoutEventHandler()
+ {
+ _logoutObserver = new NotificationObserver(OperationType.Logout);
+ _logoutObserver.NotificationReceived += (string data) =>
+ {
+ //TODO : Logout procedure
+ Console.WriteLine("Logout!");
+ };
+
+ var monitor = _dispatcher.Listen(OperationType.Logout);
+ _logoutObserver.Subscribe(monitor);
+ }
+
+ public void InitializeUserData()
+ {
+ var request = new Request(OperationType.GetData, "{ \"action\":" + (int)DataActionType.GetUpdatedUserProfile + " }");
+
+ request.ResponseReceived += (Response response) =>
+ {
+ //TODO : User Data Service
+ Console.WriteLine($"Get Updated User Profile");
+ };
+ _dispatcher.SendRequest(request);
+ }
+
+ public void InitializeConsent()
+ {
+ var request = new Request(OperationType.GetConsentStatus, "{ \"isUpToDate\": false }");
+
+ request.ResponseReceived += (Response response) =>
+ {
+ //TODO : Consent Fail Case
+ Console.WriteLine($"Get Consent Status");
+
+ SendConsentOkReturn();
+ };
+ _dispatcher.SendRequest(request);
+ }
+
+ public void InitializeUserGear()
+ {
+ var request = new Request(OperationType.GetUserGear, "{ \"isUpToDate\": false }");
+
+ request.ResponseReceived += (Response response) =>
+ {
+ Console.WriteLine($"Get User Gear Status");
+ SendGearStatusOkReturn();
+ };
+ _dispatcher.SendRequest(request);
+ }
+
+ public void InitializeSettings()
+ {
+ _settingManager.Initialize(() =>
+ {
+ GetWorkoutSettings();
+ });
+ }
+
+ private void GetWorkoutSettings()
+ {
+ var request = new Request(OperationType.GetWorkoutSettings, "");
+
+ request.ResponseReceived += (Response response) =>
+ {
+ Console.WriteLine($"Get Workout Settings");
+ };
+ _dispatcher.SendRequest(request);
+ }
+
+ public void InitializeAfterLoading()
+ {
+ _phoneService.OnAppStart();
+
+ InitializeSContext();
+
+ InitializeGpsAcquire();
+
+ InitializeTrainingPlan();
+ }
+
+ void InitializeSContext()
+ {
+ var request = new Request(OperationType.CheckSContext, "");
+
+ request.ResponseReceived += (Response response) =>
+ {
+ Console.WriteLine($"Get SContext Status");
+ };
+ _dispatcher.SendRequest(request);
+ }
+
+ void InitializeGpsAcquire()
+ {
+ var request = new Request(OperationType.StartGpsAcquire, "");
+
+ request.ResponseReceived += (Response response) =>
+ {
+ Console.WriteLine($"Start Gps Acquire");
+ };
+ _dispatcher.SendRequest(request);
+ }
+
+ void InitializeTrainingPlan()
+ {
+ var request = new Request(OperationType.GetTodayTrainingPlans, "");
+
+ request.ResponseReceived += (Response response) =>
+ {
+ Console.WriteLine($"Get Training Plans");
+ };
+ _dispatcher.SendRequest(request);
+ }
+
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRun.Models
+{
+ public class NotificationObserver : IObserver<string>
+ {
+ public OperationType Type { get; }
+ public event Action<string> NotificationReceived;
+ public event Action<Exception> NotificationErrorReceived;
+ IDisposable _cancellation;
+
+ public NotificationObserver(OperationType type)
+ {
+ Type = type;
+ }
+
+ public void Subscribe(NotificationProvider provider)
+ {
+ _cancellation = provider.Subscribe(this);
+ }
+
+ public void Unsubscribe()
+ {
+ _cancellation.Dispose();
+ }
+
+ public void OnCompleted()
+ {
+ Console.WriteLine($"Notification Observer Completed Type: {Type}");
+ }
+
+ public void OnError(Exception error)
+ {
+ NotificationErrorReceived(error);
+ }
+
+ public void OnNext(string value)
+ {
+ NotificationReceived(value);
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRun.Models
+{
+ public class NotificationProvider : IObservable<string>
+ {
+ readonly List<IObserver<string>> observers;
+ public OperationType Type { get; }
+ public string Name { get; }
+
+ public NotificationProvider(OperationType type)
+ {
+ Type = type;
+ observers = new List<IObserver<string>>();
+ }
+
+ public void PostNotification(string data)
+ {
+ foreach (var observer in observers)
+ observer.OnNext(data);
+ }
+
+ public IDisposable Subscribe(IObserver<string> observer)
+ {
+ if (!observers.Contains(observer))
+ observers.Add(observer);
+ return new Unsubscriber(observers, observer);
+ }
+
+ private class Unsubscriber : IDisposable
+ {
+ private readonly List<IObserver<string>> _observers;
+ private readonly IObserver<string> _observer;
+
+ public Unsubscriber(List<IObserver<string>> observers, IObserver<string> observer)
+ {
+ _observers = observers;
+ _observer = observer;
+ }
+
+ public void Dispose()
+ {
+ if (_observer != null && _observers.Contains(_observer))
+ _observers.Remove(_observer);
+ }
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRun.Models
+{
+ public class PermissionManager : IPermissionManager
+ {
+ const string LocationPrivilege = "http://tizen.org/privilege/location";
+ const string SensorPrivilege = "http://tizen.org/privilege/healthinfo";
+
+ bool _isLocationPermitted = false;
+ bool _isSensorPermitted = false;
+ IDispatcher _dispatcher;
+
+ public PermissionManager(IDispatcher dispatcher)
+ {
+ _dispatcher = dispatcher;
+ }
+
+ public bool HasPermission(PermissionType permission)
+ {
+ if (permission == PermissionType.Location)
+ return _isLocationPermitted;
+ else if (permission == PermissionType.Sensor)
+ return _isSensorPermitted;
+ else
+ return false;
+ }
+
+ void StoreCache(PermissionType permission, bool isPermitted)
+ {
+ if (permission == PermissionType.Location)
+ _isLocationPermitted = isPermitted;
+ else if (permission == PermissionType.Sensor)
+ _isSensorPermitted = isPermitted;
+ }
+
+ public void RequestPermission(PermissionType permission, OnPermissionResponse onResponse)
+ {
+ var privilege = "";
+ if (permission == PermissionType.Location)
+ {
+ privilege = LocationPrivilege;
+ }
+ else if (permission == PermissionType.Sensor)
+ {
+ privilege = SensorPrivilege;
+ }
+ else
+ {
+ onResponse(false, false);
+ return;
+ }
+
+ var request = new Request(OperationType.RequestPermission, privilege);
+
+ request.ResponseReceived += (Response response) =>
+ {
+ Console.WriteLine($"Get Request Permission Response {response.Data}");
+ if (response.Status != ResponseStatus.Success)
+ {
+ onResponse(false, false);
+ return;
+ }
+
+ if (response.Data.Equals("true"))
+ {
+ StoreCache(permission, true);
+ onResponse(true, true);
+ }
+ else
+ {
+ StoreCache(permission, false);
+ onResponse(true, false);
+ }
+ };
+ _dispatcher.SendRequest(request);
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRun.Models
+{
+ public enum PhoneApplicationState
+ {
+ NotSupportedTizenVersion,
+ LoggedIn,
+ Authenticated,
+ NoBTE,
+ NoInternetConnection,
+ Timeout,
+ NotInstalled,
+ NotLoggedIn,
+ NotAuthenticated,
+ NoConsentAgreement,
+ WaitingForConsentAcceptance
+ }
+
+ public class PhoneService : IPhoneService
+ {
+ private const string SwVersionPrefix = "R360";
+ private const string SwVersionSuffix = "BPK5";
+ readonly IPlatformService _platformService;
+ readonly IDispatcher _dispatcher;
+ public PhoneService(IDispatcher dispatcher, IPlatformService platformService)
+ {
+ _platformService = platformService;
+ _dispatcher = dispatcher;
+ }
+
+ public bool IsPlatformSwVersionSupported()
+ {
+ _platformService.GetSwVersion(out string version);
+
+ if (version.StartsWith(SwVersionPrefix))
+ {
+ if (string.Compare(version.Substring(version.Length - 4), SwVersionSuffix) < 0)
+ return false;
+ }
+
+ return true;
+ }
+
+ public bool IsAuthenticated(Action<PhoneApplicationState> authenticatedCb)
+ {
+ //TODO : Check cache
+ //TODO : Set Timer 120 sec
+
+ SendIsAuthenticatedRequest(authenticatedCb);
+ return true;
+ }
+
+ public void InitializeApp()
+ {
+ var request = new Request(OperationType.ApplicationInitialized, "");
+
+ request.ResponseReceived += (Response response) =>
+ {
+ // TODO : Implement Cache Storage to save watch type
+ //if (result && result.data)
+ //{
+ // this.storageService.save(DEVICE_TYPE_STORAGE_KEY, result.data.deviceType);
+ //}
+ Console.WriteLine($"Send UI App Initialization signal");
+ };
+ _dispatcher.SendRequest(request);
+ }
+
+ public void KeepScreenOn()
+ {
+ _platformService.KeepScreenOn();
+ }
+
+ public void SetAppTerminateTimer(int sec)
+ {
+ }
+
+ public void ClearAppTerminateTimer()
+ {
+ }
+
+ private void SendIsAuthenticatedRequest(Action<PhoneApplicationState> authenticatedCb)
+ {
+ var request = new Request(OperationType.CheckIsAuthenticated, "{ \"authenticate\": false }");
+
+ request.ResponseReceived += (Response response) =>
+ {
+ //TODO : Cache to keep auth status
+ //TODO : Check response data and Fail case
+ Console.WriteLine($"Get authenticate status");
+
+ authenticatedCb(PhoneApplicationState.LoggedIn);
+ };
+ _dispatcher.SendRequest(request);
+ }
+
+ public void OnAppStart()
+ {
+ var request = new Request(OperationType.ApplicationStarted, "");
+
+ request.ResponseReceived += (Response response) =>
+ {
+ Console.WriteLine("App Start!");
+ };
+ _dispatcher.SendRequest(request);
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace MapMyRun.Models
+{
+ public enum OperationType
+ {
+ PrepareWKO = 1,
+ StartWKO,
+ PauseWKO,
+ ResumeWKO,
+ FinishWKO,
+ SaveWKO,
+ DiscardWKO,
+ CheckSContext,
+ CheckIsLoggedIn,
+ NavigateToLogin,
+ NavigateToProfile,
+ CheckIsAuthenticated,
+ CheckIsInstalled,
+ GetData,
+ UpdateNutritionPreference,
+ GetSleepScore,
+ ExitApp,
+ MFPGetCurrentSteps,
+ MFPAddEnergy,
+ MFPAddWater,
+ GetSettings,
+ SetSettings,
+ GetWorkoutSettings,
+ SetWorkoutSettings,
+ UnfinishedWorkoutRestore,
+ RestoreWKO,
+ StartGpsAcquire,
+ StopGpsAcquire,
+ CheckNetworkConnection,
+ ChangeNutritionSource,
+ GetConsentStatus,
+ GetUserGear,
+ ConnectUserGear,
+ DisconnectUserGear,
+ RequestPermission,
+ GetTodayTrainingPlans,
+ // More operations to be here
+ OperationMax = GetTodayTrainingPlans,
+ // Notification Type Start
+ WkoDataChanged,
+ HRData,
+ AggregatesData,
+ HRZone,
+ IntensityChanged,
+ GetDiarySummary,
+ GetCachedDiarySummary,
+ StepsChanged,
+ SapInitialized,
+ ApplicationInitialized,
+ ApplicationStarted,
+ NoBluetoothConnection,
+ WristUpEvent,
+ MfpDataUpdated,
+ SapRemoteServiceNotFound,
+ WillpowerChanged,
+ EnergyChanged,
+ SpeedChanged,
+ GpsStateChanged,
+ ShoesStateChanged,
+ MeasurementPreferenceChanged,
+ NutritionPreferencesUpdated,
+ SleepScoreReceived,
+ WorkoutSyncSuccess,
+ WorkoutSyncFailed,
+ UserProfileUpdated,
+ LocationChanged,
+ Logout,
+ SettingsChanged,
+ WorkoutSettingsChanged,
+ SapConnectionTimeout,
+ LangChanged,
+ HapticFeedback,
+ NutritionSourceSupportChanged,
+ CadenceChanged,
+ StrideLengthChanged,
+ RTFCUpdate,
+ RTFCNotification,
+ RTSCUpdate,
+ RTSCNotification,
+ GoalUpdate,
+ GoalCompleted,
+ AppStateForeground,
+ AppStateBackground,
+ // More operations to be here
+ NotificationTypeMax = AppStateBackground
+ }
+
+ public enum DataActionType
+ {
+ None = 0,
+ GetUserProfile,
+ GetUpdatedUserProfile,
+ PostWorkout,
+ GetUserGoals,
+ GetUpToDateUserGoals,
+ GetHrZones,
+ GetChallenges,
+ GetChallengeDetails,
+ GetWorkouts
+ }
+
+ public class Request
+ {
+ public OperationType Operation { get; }
+ public string Data { get; }
+ public event Action<Response> ResponseReceived;
+ public string TransID { get; set; }
+
+ public Request (OperationType operation, string data)
+ {
+ Operation = operation;
+ Data = data;
+ }
+
+ public void OnReceiveResponse(Response response)
+ {
+ ResponseReceived?.Invoke(response);
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace MapMyRun.Models
+{
+ public enum ResponseStatus
+ {
+ Success = 0,
+ Unsupported,
+ Fail
+ }
+
+ public class Response
+ {
+ public string Data { get; }
+ public string TransID { get; }
+ public ResponseStatus Status { get; }
+
+ public Response(string tid, string data, ResponseStatus status)
+ {
+ TransID = tid;
+ Data = data;
+ Status = status;
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRun.Models.Settings
+{
+ public interface ISettingManager
+ {
+ void Initialize(Action next);
+
+ event Action<GPSState> GpsStateChanged;
+ }
+}
--- /dev/null
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRun.Models.Settings
+{
+ public enum GPSState
+ {
+ Off,
+ Ready,
+ DisabledForActivity,
+ Acquiring,
+ Acquired,
+ Error
+ }
+
+ public class SettingManager : ISettingManager
+ {
+ readonly IDispatcher _dispatcher;
+ public event Action<GPSState> GpsStateChanged;
+ NotificationObserver _gpsStateObserver;
+ bool isGpsEnabled = false;
+
+ private GPSState _gpsState = GPSState.Off;
+ public GPSState GpsState
+ {
+ get
+ {
+ if (!isGpsEnabled)
+ {
+ return GPSState.Off;
+ }
+ else
+ {
+ return _gpsState;
+ }
+ }
+ set
+ {
+ if (_gpsState == value)
+ return;
+ if (!isGpsEnabled)
+ {
+ _gpsState = GPSState.Off;
+ } else
+ {
+ _gpsState = value;
+ }
+
+ GpsStateChanged?.Invoke(_gpsState);
+ }
+ }
+
+ public SettingManager(IDispatcher dispatcher)
+ {
+ _dispatcher = dispatcher;
+ }
+
+ public void Initialize(Action next)
+ {
+ _gpsStateObserver = new NotificationObserver(OperationType.GpsStateChanged);
+ _gpsStateObserver.NotificationReceived += (string data) =>
+ {
+ Console.WriteLine("Gps Changed Received");
+ try
+ {
+ if (Int32.Parse(data) == 0)
+ GpsState = GPSState.Acquiring;
+ else
+ GpsState = GPSState.Acquired;
+ }
+ catch
+ {
+ Console.WriteLine("Fail to parse notification");
+ }
+ };
+
+ var monitor = _dispatcher.Listen(OperationType.GpsStateChanged);
+ _gpsStateObserver.Subscribe(monitor);
+
+ var request = new Request(OperationType.GetSettings, "");
+ request.ResponseReceived += (Response response) =>
+ {
+ Console.WriteLine($"Receive Get Settings");
+ if (response.Status == ResponseStatus.Success)
+ OnReceiveSettingsDataString(response.Data);
+
+ next?.Invoke();
+ };
+ _dispatcher.SendRequest(request);
+ }
+
+ void OnReceiveSettingsDataString(string jsonString)
+ {
+ SettingsData settingsData = ParseSettingsDataJson(jsonString);
+ if (settingsData == null)
+ return;
+
+ if (settingsData.gps != null)
+ {
+ if (settingsData.gps.isEnabled && settingsData.gps.systemValue)
+ {
+ isGpsEnabled = true;
+ GpsState = GPSState.Ready;
+ }
+ else
+ {
+ isGpsEnabled = false;
+ GpsState = GPSState.Off;
+ }
+ }
+ }
+
+ public SettingsData ParseSettingsDataJson(string jsonString)
+ {
+ SettingsData settingsData= null;
+ try
+ {
+ settingsData = JsonConvert.DeserializeObject<SettingsData>(jsonString);
+ }
+ catch
+ {
+ Console.WriteLine($"Settings DATA JSON Parse Fail: {jsonString}");
+ }
+ return settingsData;
+ }
+
+ private void StartGpsAcquire()
+ {
+ var request = new Request(OperationType.StartGpsAcquire, "");
+ request.ResponseReceived += (Response response) =>
+ {
+ if (response.Status == ResponseStatus.Success)
+ {
+ GpsState = GPSState.Acquiring;
+ }
+ };
+ _dispatcher.SendRequest(request);
+ }
+
+ private void StopGpsAcquire()
+ {
+ var request = new Request(OperationType.StopGpsAcquire, "");
+ request.ResponseReceived += (Response response) =>
+ {
+ if (response.Status == ResponseStatus.Success)
+ {
+ GpsState = GPSState.Off;
+ }
+ };
+ _dispatcher.SendRequest(request);
+ }
+
+ public void ToggleGPS(bool gps)
+ {
+ isGpsEnabled = gps;
+
+ if (!isGpsEnabled)
+ {
+ StopGpsAcquire();
+ } else
+ {
+ StartGpsAcquire();
+ }
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRun.Models.Settings
+{
+ public class SettingsData
+ {
+ public GpsSettingData gps { get; set; }
+ public DebugSettingData debug { get; set; }
+ public ScreenSettingData screenTimeout { get; set; }
+ }
+
+ public class GpsSettingData
+ {
+ public bool systemValue { get; set; }
+ public bool isEnabled { get; set; }
+ public bool isSupported { get; set; }
+ }
+
+ public class DebugSettingData
+ {
+ public FileLogger fileLogger { get; set; }
+ public bool isEnabled { get; set; }
+ public bool isSupported { get; set; }
+ }
+
+ public class ScreenSettingData
+ {
+ public int value { get; set; }
+ public bool isEnabled { get; set; }
+ public bool isSupported { get; set; }
+ }
+
+ public class FileLogger
+ {
+ public bool isEnabled { get; set; }
+ public bool isSupported { get; set; }
+ }
+}
\ No newline at end of file
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRun.Models.Workout.Items
+{
+ class AverageCadence : IWorkoutItem
+ {
+ string _value = "--";
+ public event Action<string> ValueChanged;
+ public string Value
+ {
+ get
+ {
+ return _value;
+ }
+ }
+
+ public WorkoutItemType GetWorkoutItemType()
+ {
+ return WorkoutItemType.AvgCadence;
+ }
+
+ public string GetEmptyValue()
+ {
+ return "--";
+ }
+
+ public string GetImage()
+ {
+ return "/assets/workout.cadence.png";
+ }
+
+ public string GetLabel(WorkoutItemLabelType type)
+ {
+ switch (type)
+ {
+ case WorkoutItemLabelType.GeneralLabel:
+ default:
+ return "label_cadence_avg";
+ case WorkoutItemLabelType.ShortLabel:
+ return "label_cadence_avg_short";
+ case WorkoutItemLabelType.MediumLabel:
+ return "label_cadence_avg_medium";
+ case WorkoutItemLabelType.LongLabel:
+ return "label_cadence_avg_long";
+ }
+ }
+
+ public string GetLabelStyle()
+ {
+ return "";
+ }
+
+ public string GetStatsStyle()
+ {
+ return "";
+ }
+
+ public string GetUnit()
+ {
+ return "workout_steps_per_min";
+ }
+
+ public void OnDataReceived(WorkoutData data)
+ {
+ if (data.aggregates == null || data.aggregates.cadence_avg <= 0)
+ return;
+
+ var newValue = ConvertValue(data.aggregates.cadence_avg);
+ if (_value.Equals(newValue))
+ return;
+
+ _value = newValue;
+ ValueChanged?.Invoke(_value);
+ Console.WriteLine($"AverageCadence Data Changed: {_value}");
+ }
+
+ string ConvertValue(float value)
+ {
+ var convertedCadence = value;
+ var readableValue = convertedCadence > 0 ? convertedCadence.ToString() : "--";
+ return readableValue;
+ }
+
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRun.Models.Workout.Items
+{
+ class AverageHr : IWorkoutItem
+ {
+ string _value = "--";
+ public event Action<string> ValueChanged;
+ public string Value
+ {
+ get
+ {
+ return _value;
+ }
+ }
+
+ public WorkoutItemType GetWorkoutItemType()
+ {
+ return WorkoutItemType.AvgHR;
+ }
+
+ public string GetEmptyValue()
+ {
+ return "--";
+ }
+
+ public string GetImage()
+ {
+ return "/assets/workout.hr.png";
+ }
+
+ public string GetLabel(WorkoutItemLabelType type)
+ {
+ switch (type)
+ {
+ case WorkoutItemLabelType.GeneralLabel:
+ default:
+ return "AVG HR";
+ case WorkoutItemLabelType.ShortLabel:
+ return "AVG HR";
+ case WorkoutItemLabelType.MediumLabel:
+ return "AVG HR";
+ case WorkoutItemLabelType.LongLabel:
+ return "AVG HEART RATE(BPM)";
+ }
+ }
+
+ public string GetLabelStyle()
+ {
+ return "";
+ }
+
+ public string GetStatsStyle()
+ {
+ return "";
+ }
+
+ public string GetUnit()
+ {
+ return "bpm";
+ }
+
+ public void OnDataReceived(WorkoutData data)
+ {
+ var newValue = data.hrData.avg.ToString();
+ if (_value.Equals(newValue))
+ return;
+
+ _value = newValue;
+ ValueChanged?.Invoke(_value);
+ Console.WriteLine($"AverageHr Data Changed: {_value}");
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRun.Models.Workout.Items
+{
+ class AveragePace : IWorkoutItem
+ {
+ string _value = "--";
+ const int MAX_PACE = 3600;
+ public event Action<string> ValueChanged;
+ public string Value
+ {
+ get
+ {
+ return _value;
+ }
+ }
+
+ public WorkoutItemType GetWorkoutItemType()
+ {
+ return WorkoutItemType.AvgPace;
+ }
+
+ public string GetEmptyValue()
+ {
+ return "--";
+ }
+
+ public string GetImage()
+ {
+ return "/assets/workout.pace.png";
+ }
+
+ public string GetLabel(WorkoutItemLabelType type)
+ {
+ switch (type)
+ {
+ case WorkoutItemLabelType.GeneralLabel:
+ default:
+ return "label_pace_avg";
+ case WorkoutItemLabelType.ShortLabel:
+ return "label_pace_avg_short";
+ case WorkoutItemLabelType.MediumLabel:
+ return "label_pace_avg_medium";
+ case WorkoutItemLabelType.LongLabel:
+ return "label_pace_avg_long";
+ }
+ }
+
+ public string GetLabelStyle()
+ {
+ return "";
+ }
+
+ public string GetStatsStyle()
+ {
+ return "";
+ }
+
+ public string GetUnit()
+ {
+ return "workout_min_per_km";
+ }
+
+ public void OnDataReceived(WorkoutData data)
+ {
+ if (data.aggregates == null || data.aggregates.active_time_total < 0 || data.aggregates.distance_total < 0)
+ return;
+
+ var newValue = ConvertValue(data.aggregates.active_time_total / data.aggregates.distance_total);
+ if (_value.Equals(newValue))
+ return;
+
+ _value = newValue;
+ ValueChanged?.Invoke(_value);
+ Console.WriteLine($"AveragePace Data Changed: {_value}");
+ }
+
+ string ConvertValue(float value)
+ {
+ var convertedPace = Math.Round(value);
+ var readableValue = convertedPace > 0 && convertedPace <= MAX_PACE ? convertedPace.ToString() : "--";
+ return readableValue;
+ }
+
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRun.Models.Workout.Items
+{
+ class AverageSpeed : IWorkoutItem
+ {
+ string _value = "--";
+ public event Action<string> ValueChanged;
+ public string Value
+ {
+ get
+ {
+ return _value;
+ }
+ }
+
+ public WorkoutItemType GetWorkoutItemType()
+ {
+ return WorkoutItemType.AvgSpeed;
+ }
+
+ public string GetEmptyValue()
+ {
+ return "--";
+ }
+
+ public string GetImage()
+ {
+ return "/assets/workout.pace.png";
+ }
+
+ public string GetLabel(WorkoutItemLabelType type)
+ {
+ switch (type)
+ {
+ case WorkoutItemLabelType.GeneralLabel:
+ default:
+ return "label_speed_avg";
+ case WorkoutItemLabelType.ShortLabel:
+ return "label_speed_avg_short";
+ case WorkoutItemLabelType.MediumLabel:
+ return "label_speed_avg_medium";
+ case WorkoutItemLabelType.LongLabel:
+ return "label_speed_avg_long";
+ }
+ }
+
+ public string GetLabelStyle()
+ {
+ return "";
+ }
+
+ public string GetStatsStyle()
+ {
+ return "";
+ }
+
+ public string GetUnit()
+ {
+ return "workout_km_per_hr";
+ }
+
+ public void OnDataReceived(WorkoutData data)
+ {
+ if (data.aggregates == null || data.aggregates.active_time_total < 0 || data.aggregates.distance_total < 0)
+ return;
+
+ var newValue = ConvertValue(data.aggregates.distance_total / data.aggregates.active_time_total);
+ if (_value.Equals(newValue))
+ return;
+
+ _value = newValue;
+ ValueChanged?.Invoke(_value);
+ Console.WriteLine($"AverageSpeed Data Changed: {_value}");
+ }
+
+ string ConvertValue(float value)
+ {
+ var convertedSpeed = Math.Round(value * 100) / 100;
+ var readableValue = convertedSpeed > 0 ? convertedSpeed.ToString() : "--";
+ return readableValue;
+ }
+
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRun.Models.Workout.Items
+{
+ class Calories : IWorkoutItem
+ {
+ string _value = "--";
+ public event Action<string> ValueChanged;
+
+ public WorkoutItemType GetWorkoutItemType()
+ {
+ return WorkoutItemType.Calories;
+ }
+
+ public string GetEmptyValue()
+ {
+ return "--";
+ }
+
+ public string GetImage()
+ {
+ return "/assets/workout.calories.png";
+ }
+
+ public string GetLabel(WorkoutItemLabelType type)
+ {
+ switch (type)
+ {
+ case WorkoutItemLabelType.GeneralLabel:
+ default:
+ return "Calories";
+ case WorkoutItemLabelType.ShortLabel:
+ return "Calories";
+ case WorkoutItemLabelType.MediumLabel:
+ return "Calories";
+ case WorkoutItemLabelType.LongLabel:
+ return "Calories";
+ }
+ }
+
+ public string GetLabelStyle()
+ {
+ return "";
+ }
+
+ public string GetStatsStyle()
+ {
+ return "";
+ }
+
+ public string GetUnit()
+ {
+ return "";
+ }
+
+ public string GetValue()
+ {
+ return _value;
+ }
+
+ public void OnDataReceived(WorkoutData data)
+ {
+ if (_value.Equals(data.calories))
+ return;
+
+ _value = data.calories;
+ ValueChanged?.Invoke(_value);
+ Console.WriteLine($"Calories Data Changed: {_value}");
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRun.Models.Workout.Items
+{
+ class CurrentCadence : IWorkoutItem
+ {
+ string _value = "--";
+ public event Action<string> ValueChanged;
+ public string Value
+ {
+ get
+ {
+ return _value;
+ }
+ }
+
+ public WorkoutItemType GetWorkoutItemType()
+ {
+ return WorkoutItemType.Cadence;
+ }
+
+ public string GetEmptyValue()
+ {
+ return "--";
+ }
+
+ public string GetImage()
+ {
+ return "/assets/workout.cadence.png";
+ }
+
+ public string GetLabel(WorkoutItemLabelType type)
+ {
+ switch (type)
+ {
+ case WorkoutItemLabelType.GeneralLabel:
+ default:
+ return "CADENCE";
+ case WorkoutItemLabelType.ShortLabel:
+ return "CADENCE";
+ case WorkoutItemLabelType.MediumLabel:
+ return "CADENCE";
+ case WorkoutItemLabelType.LongLabel:
+ return "CADENCE(SPM)";
+ }
+ }
+
+ public string GetLabelStyle()
+ {
+ return "";
+ }
+
+ public string GetStatsStyle()
+ {
+ return "";
+ }
+
+ public string GetUnit()
+ {
+ return "workout_steps_per_min";
+ }
+
+ public void OnDataReceived(WorkoutData data)
+ {
+ if (data.cadence == null || data.cadence.latest < 0)
+ return;
+
+ var newValue = ConvertValue(data.cadence.latest);
+ if (_value.Equals(newValue))
+ return;
+
+ _value = newValue;
+ ValueChanged?.Invoke(_value);
+ Console.WriteLine($"CurrentCadence Data Changed: {_value}");
+ }
+
+ string ConvertValue(float value)
+ {
+ var convertedCadence = value;
+ var readableValue = convertedCadence > 0 ? convertedCadence.ToString() : "--";
+ return readableValue;
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRun.Models.Workout.Items
+{
+ class CurrentHr : IWorkoutItem
+ {
+ string _value = "--";
+ public event Action<string> ValueChanged;
+ public string Value
+ {
+ get
+ {
+ return _value;
+ }
+ }
+
+ public WorkoutItemType GetWorkoutItemType()
+ {
+ return WorkoutItemType.CurrentHR;
+ }
+
+ public string GetEmptyValue()
+ {
+ return "--";
+ }
+
+ public string GetImage()
+ {
+ return "/assets/workout.hr.png";
+ }
+
+ public string GetLabel(WorkoutItemLabelType type)
+ {
+ //TODO : i18n
+ switch (type)
+ {
+ case WorkoutItemLabelType.GeneralLabel:
+ default:
+ return "HR";
+ case WorkoutItemLabelType.ShortLabel:
+ return "HR";
+ case WorkoutItemLabelType.MediumLabel:
+ return "HR";
+ case WorkoutItemLabelType.LongLabel:
+ return "HEART RATE(BPM)";
+ }
+ }
+
+ public string GetLabelStyle()
+ {
+ return "";
+ }
+
+ public string GetStatsStyle()
+ {
+ return "";
+ }
+
+ public string GetUnit()
+ {
+ return "bpm";
+ }
+
+ public void OnDataReceived(WorkoutData data)
+ {
+ var newValue = data.hrData.latest.ToString();
+ if (_value.Equals(newValue))
+ return;
+
+ _value = newValue;
+ ValueChanged?.Invoke(_value);
+ Console.WriteLine($"CurrentHr Data Changed: {_value}");
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRun.Models.Workout.Items
+{
+ class CurrentPace : IWorkoutItem
+ {
+ string _value = "--";
+ const int MAX_PACE = 3600;
+ public event Action<string> ValueChanged;
+ public string Value
+ {
+ get
+ {
+ return _value;
+ }
+ }
+
+ public WorkoutItemType GetWorkoutItemType()
+ {
+ return WorkoutItemType.Pace;
+ }
+
+ public string GetEmptyValue()
+ {
+ return "--";
+ }
+
+ public string GetImage()
+ {
+ return "/assets/workout.pace.png";
+ }
+
+ public string GetLabel(WorkoutItemLabelType type)
+ {
+ switch (type)
+ {
+ case WorkoutItemLabelType.GeneralLabel:
+ default:
+ return "PACE";
+ case WorkoutItemLabelType.ShortLabel:
+ return "PACE";
+ case WorkoutItemLabelType.MediumLabel:
+ return "PACE";
+ case WorkoutItemLabelType.LongLabel:
+ return "PACE(MIN/KM)";
+ }
+ }
+
+ public string GetLabelStyle()
+ {
+ return "";
+ }
+
+ public string GetStatsStyle()
+ {
+ return "";
+ }
+
+ public string GetUnit()
+ {
+ return "workout_min_per_km";
+ }
+
+ public void OnDataReceived(WorkoutData data)
+ {
+ if (data.speed == null || data.speed.latest < 0)
+ return;
+
+ var newValue = ConvertValue(data.speed.latest);
+ if (_value.Equals(newValue))
+ return;
+
+ _value = newValue;
+ ValueChanged?.Invoke(_value);
+ Console.WriteLine($"CurrentPace Data Changed: {_value}");
+ }
+
+ string ConvertValue(float value)
+ {
+ var convertedPace = Math.Round(value);
+ var readableValue = convertedPace > 0 && convertedPace <= MAX_PACE ? convertedPace.ToString() : "--";
+ return readableValue;
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRun.Models.Workout.Items
+{
+ class CurrentSpeed : IWorkoutItem
+ {
+ string _value = "--";
+ public event Action<string> ValueChanged;
+ public string Value
+ {
+ get
+ {
+ return _value;
+ }
+ }
+
+ public WorkoutItemType GetWorkoutItemType()
+ {
+ return WorkoutItemType.Speed;
+ }
+
+ public string GetEmptyValue()
+ {
+ return "--";
+ }
+
+ public string GetImage()
+ {
+ return "/assets/workout.pace.png";
+ }
+
+ public string GetLabel(WorkoutItemLabelType type)
+ {
+ switch (type)
+ {
+ case WorkoutItemLabelType.GeneralLabel:
+ default:
+ return "label_speed";
+ case WorkoutItemLabelType.ShortLabel:
+ return "label_speed_short";
+ case WorkoutItemLabelType.MediumLabel:
+ return "label_speed_medium";
+ case WorkoutItemLabelType.LongLabel:
+ return "label_speed_long";
+ }
+ }
+
+ public string GetLabelStyle()
+ {
+ return "";
+ }
+
+ public string GetStatsStyle()
+ {
+ return "";
+ }
+
+ public string GetUnit()
+ {
+ return "workout_km_per_hr";
+ }
+
+ public void OnDataReceived(WorkoutData data)
+ {
+ if (data.speed.latest < 0)
+ return;
+
+ var newValue = ConvertValue(data.speed.latest);
+ if (_value.Equals(newValue))
+ return;
+
+ _value = newValue;
+ ValueChanged?.Invoke(_value);
+ Console.WriteLine($"CurrentSpeed Data Changed: {_value}");
+ }
+
+ string ConvertValue(float value)
+ {
+ var convertedSpeed = Math.Round(value * 100) / 100;
+ var readableValue = convertedSpeed > 0 ? convertedSpeed.ToString() : "--";
+ return readableValue;
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRun.Models.Workout.Items
+{
+ class Distance : IWorkoutItem
+ {
+ string _value = "--";
+ public event Action<string> ValueChanged;
+ public string Value
+ {
+ get
+ {
+ return _value;
+ }
+ }
+
+ public WorkoutItemType GetWorkoutItemType()
+ {
+ return WorkoutItemType.Distance;
+ }
+
+ public string GetEmptyValue()
+ {
+ return "--";
+ }
+
+ public string GetImage()
+ {
+ return "/assets/workout.distance.png";
+ }
+
+ public string GetLabel(WorkoutItemLabelType type)
+ {
+ switch (type)
+ {
+ case WorkoutItemLabelType.GeneralLabel:
+ default:
+ return "DISTANCE";
+ case WorkoutItemLabelType.ShortLabel:
+ return "DISTANCE";
+ case WorkoutItemLabelType.MediumLabel:
+ return "DISTANCE";
+ case WorkoutItemLabelType.LongLabel:
+ return "DISTANCE(KM)";
+ }
+ }
+
+ public string GetLabelStyle()
+ {
+ return "";
+ }
+
+ public string GetStatsStyle()
+ {
+ return "";
+ }
+
+ public string GetUnit()
+ {
+ return "workout_km";
+ }
+
+ public void OnDataReceived(WorkoutData data)
+ {
+ if (data == null || data.aggregates == null)
+ return;
+
+ var newValue = data.aggregates.distance_total.ToString();
+
+ if (_value.Equals(newValue))
+ return;
+
+ _value = newValue;
+ ValueChanged?.Invoke(_value);
+ Console.WriteLine($"Distance Data Changed: {_value}");
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRun.Models.Workout.Items
+{
+ class Duration : IWorkoutItem
+ {
+ string _value = "--";
+ public event Action<string> ValueChanged;
+ public string Value
+ {
+ get
+ {
+ return _value;
+ }
+ }
+
+ public WorkoutItemType GetWorkoutItemType()
+ {
+ return WorkoutItemType.Duration;
+ }
+
+ public string GetEmptyValue()
+ {
+ return "--";
+ }
+
+ public string GetImage()
+ {
+ return "/assets/workout.duration.png";
+ }
+
+ public string GetLabel(WorkoutItemLabelType type)
+ {
+ switch (type)
+ {
+ case WorkoutItemLabelType.GeneralLabel:
+ default:
+ return "DURATION";
+ case WorkoutItemLabelType.ShortLabel:
+ return "DURATION";
+ case WorkoutItemLabelType.MediumLabel:
+ return "DURATION";
+ case WorkoutItemLabelType.LongLabel:
+ return "DURATION";
+ }
+ }
+
+ public string GetLabelStyle()
+ {
+ return "";
+ }
+
+ public string GetStatsStyle()
+ {
+ return "";
+ }
+
+ public string GetUnit()
+ {
+ return null;
+ }
+
+ public void OnDataReceived(WorkoutData data)
+ {
+ if (data == null || data.aggregates == null)
+ return;
+
+ var newValue = data.aggregates.active_time_total.ToString();
+
+ if (_value.Equals(newValue))
+ return;
+
+ _value = newValue;
+ ValueChanged?.Invoke(_value);
+ Console.WriteLine($"Duration Data Changed: {_value}");
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRun.Models.Workout.Items
+{
+ public enum WorkoutItemType
+ {
+ Duration,
+ Distance,
+ Pace,
+ AvgPace,
+ MaxPace,
+ Speed,
+ AvgSpeed,
+ MaxSpeed,
+ Cadence,
+ AvgCadence,
+ StrideLength,
+ AvgStrideLength,
+ Steps,
+ Calories,
+ CurrentHR,
+ HRZone,
+ AvgHR,
+ PeakHR,
+ Intensity,
+ WillPower,
+ CadenceTargetRange,
+ StrideTargetRange,
+ CadenceRangeRate,
+ DistanceTargetRange,
+ DurationTargetRange,
+ PaceTargetRange,
+ PaceRangeRate,
+ SpeedTargetRange,
+ SpeedRangeRate
+ }
+
+ public enum WorkoutItemLabelType
+ {
+ GeneralLabel,
+ ShortLabel,
+ MediumLabel,
+ LongLabel
+ }
+
+ public interface IWorkoutItem
+ {
+ WorkoutItemType GetWorkoutItemType();
+
+ string GetImage();
+
+ string GetLabel(WorkoutItemLabelType type);
+
+ string GetEmptyValue();
+
+ string GetUnit();
+
+ string GetStatsStyle();
+
+ string GetLabelStyle();
+
+ void OnDataReceived(WorkoutData data);
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRun.Models.Workout.Items
+{
+ class MaxPace : IWorkoutItem
+ {
+ string _value = "--";
+ const int MAX_PACE = 3600;
+ public event Action<string> ValueChanged;
+ public string Value
+ {
+ get
+ {
+ return _value;
+ }
+ }
+
+ public WorkoutItemType GetWorkoutItemType()
+ {
+ return WorkoutItemType.MaxPace;
+ }
+
+ public string GetEmptyValue()
+ {
+ return "--";
+ }
+
+ public string GetImage()
+ {
+ return "/assets/workout.pace.png";
+ }
+
+ public string GetLabel(WorkoutItemLabelType type)
+ {
+ switch (type)
+ {
+ case WorkoutItemLabelType.GeneralLabel:
+ default:
+ return "MAX PACE";
+ case WorkoutItemLabelType.ShortLabel:
+ return "MAX PACE";
+ case WorkoutItemLabelType.MediumLabel:
+ return "MAX PACE";
+ case WorkoutItemLabelType.LongLabel:
+ return "MAX PACE(MIN/KM)";
+ }
+ }
+
+ public string GetLabelStyle()
+ {
+ return "";
+ }
+
+ public string GetStatsStyle()
+ {
+ return "";
+ }
+
+ public string GetUnit()
+ {
+ return "workout_min_per_km";
+ }
+
+ public void OnDataReceived(WorkoutData data)
+ {
+ if (data.aggregates.speed_max <= 0)
+ return;
+
+ var newValue = ConvertValue(1 / data.aggregates.speed_max);
+ if (_value.Equals(newValue))
+ return;
+
+ _value = newValue;
+ ValueChanged?.Invoke(_value);
+ Console.WriteLine($"MaxPace Data Changed: {_value}");
+ }
+
+ string ConvertValue(float value)
+ {
+ var convertedPace = Math.Round(value);
+ var readableValue = convertedPace > 0 && convertedPace <= MAX_PACE ? convertedPace.ToString() : "--";
+ return readableValue;
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRun.Models.Workout.Items
+{
+ class MaxSpeed : IWorkoutItem
+ {
+ string _value = "--";
+ public event Action<string> ValueChanged;
+ public string Value
+ {
+ get
+ {
+ return _value;
+ }
+ }
+
+ public WorkoutItemType GetWorkoutItemType()
+ {
+ return WorkoutItemType.MaxSpeed;
+ }
+
+ public string GetEmptyValue()
+ {
+ return "--";
+ }
+
+ public string GetImage()
+ {
+ return "/assets/workout.pace.png";
+ }
+
+ public string GetLabel(WorkoutItemLabelType type)
+ {
+ switch (type)
+ {
+ case WorkoutItemLabelType.GeneralLabel:
+ default:
+ return "label_speed_max";
+ case WorkoutItemLabelType.ShortLabel:
+ return "label_speed_max_short";
+ case WorkoutItemLabelType.MediumLabel:
+ return "label_speed_max_medium";
+ case WorkoutItemLabelType.LongLabel:
+ return "label_speed_max_long";
+ }
+ }
+
+ public string GetLabelStyle()
+ {
+ return "";
+ }
+
+ public string GetStatsStyle()
+ {
+ return "";
+ }
+
+ public string GetUnit()
+ {
+ return "workout_km_per_hr";
+ }
+
+ public void OnDataReceived(WorkoutData data)
+ {
+ if (data.aggregates ==null || data.aggregates.speed_max < 0)
+ return;
+
+ var newValue = ConvertValue(data.aggregates.speed_max);
+ if (_value.Equals(newValue))
+ return;
+
+ _value = newValue;
+ ValueChanged?.Invoke(_value);
+ Console.WriteLine($"MaxSpeed Data Changed: {_value}");
+ }
+
+ string ConvertValue(float value)
+ {
+ var convertedSpeed = Math.Round(value * 100) / 100;
+ var readableValue = convertedSpeed > 0 ? convertedSpeed.ToString() : "--";
+ return readableValue;
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRun.Models.Workout.Items
+{
+ class PeakHr : IWorkoutItem
+ {
+ string _value = "--";
+ public event Action<string> ValueChanged;
+ public string Value
+ {
+ get
+ {
+ return _value;
+ }
+ }
+
+ public WorkoutItemType GetWorkoutItemType()
+ {
+ return WorkoutItemType.PeakHR;
+ }
+
+ public string GetEmptyValue()
+ {
+ return "--";
+ }
+
+ public string GetImage()
+ {
+ return "/assets/workout.hr.png";
+ }
+
+ public string GetLabel(WorkoutItemLabelType type)
+ {
+ switch (type)
+ {
+ case WorkoutItemLabelType.GeneralLabel:
+ default:
+ return "MAX HR";
+ case WorkoutItemLabelType.ShortLabel:
+ return "MAX HR";
+ case WorkoutItemLabelType.MediumLabel:
+ return "MAX HR";
+ case WorkoutItemLabelType.LongLabel:
+ return "MAX HEART RATE(BPM)";
+ }
+ }
+
+ public string GetLabelStyle()
+ {
+ return "";
+ }
+
+ public string GetStatsStyle()
+ {
+ return "";
+ }
+
+ public string GetUnit()
+ {
+ return "bpm";
+ }
+
+ public void OnDataReceived(WorkoutData data)
+ {
+ var newValue = data.hrData.max.ToString();
+ if (_value.Equals(newValue))
+ return;
+
+ _value = newValue;
+ ValueChanged?.Invoke(_value);
+ Console.WriteLine($"PeakHr Data Changed: {_value}");
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRun.Models.Workout.Items
+{
+ public class WorkoutItemFactory
+ {
+ public static IWorkoutItem CreateWorkoutItem(WorkoutItemType type)
+ {
+ Console.WriteLine($"Create WorkoutItem {type}");
+ switch (type)
+ {
+ case WorkoutItemType.Cadence:
+ return new CurrentCadence();
+ case WorkoutItemType.AvgCadence:
+ return new AverageCadence();
+ case WorkoutItemType.Pace:
+ return new CurrentPace();
+ case WorkoutItemType.AvgPace:
+ return new AveragePace();
+ case WorkoutItemType.MaxPace:
+ return new MaxPace();
+ case WorkoutItemType.Distance:
+ return new Distance();
+ case WorkoutItemType.Speed:
+ return new CurrentSpeed();
+ case WorkoutItemType.AvgSpeed:
+ return new AverageSpeed();
+ case WorkoutItemType.MaxSpeed:
+ return new MaxSpeed();
+ case WorkoutItemType.AvgHR:
+ return new AverageHr();
+ case WorkoutItemType.CurrentHR:
+ return new CurrentHr();
+ case WorkoutItemType.PeakHR:
+ return new PeakHr();
+ case WorkoutItemType.Calories:
+ return new Calories();
+ case WorkoutItemType.Duration:
+ return new Duration();
+ case WorkoutItemType.HRZone:
+ return new ZoneHr();
+ default:
+ return null;
+ }
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRun.Models.Workout.Items
+{
+ class ZoneHr : IWorkoutItem
+ {
+ string _value = "--";
+ public event Action<string> ValueChanged;
+ public string Value
+ {
+ get
+ {
+ return _value;
+ }
+ }
+
+ public WorkoutItemType GetWorkoutItemType()
+ {
+ return WorkoutItemType.HRZone;
+ }
+
+ public string GetEmptyValue()
+ {
+ return "--";
+ }
+
+ public string GetImage()
+ {
+ return "/assets/workout.hr.png";
+ }
+
+ public string GetLabel(WorkoutItemLabelType type)
+ {
+ switch (type)
+ {
+ case WorkoutItemLabelType.GeneralLabel:
+ default:
+ return "ZONE";
+ case WorkoutItemLabelType.ShortLabel:
+ return "ZONE";
+ case WorkoutItemLabelType.MediumLabel:
+ return "ZONE";
+ case WorkoutItemLabelType.LongLabel:
+ return "ZONE HEART RATE(BPM)";
+ }
+ }
+
+ public string GetLabelStyle()
+ {
+ return "";
+ }
+
+ public string GetStatsStyle()
+ {
+ return "";
+ }
+
+ public string GetUnit()
+ {
+ return "bpm";
+ }
+
+ public void OnDataReceived(WorkoutData data)
+ {
+ if (_value.Equals(data.hrZone))
+ return;
+
+ _value = data.hrZone;
+ ValueChanged?.Invoke(_value);
+ Console.WriteLine($"hrZone Data Changed: {_value}");
+ }
+ }
+}
--- /dev/null
+using MapMyRun.Models.Workout.Items;
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRun.Models.Workout
+{
+ public class Workout
+ {
+ bool _isProgressing = false;
+ bool _isListening = false;
+ WorkoutStatus _status = WorkoutStatus.NotStarted;
+ IDispatcher _dispatcher;
+ IPermissionManager _permissionManager;
+ NotificationObserver _workingDataObserver;
+ Dictionary<int, IWorkoutItem> _workoutItems = new Dictionary<int, IWorkoutItem>();
+
+ public event Action<WorkoutData> CaloriesDataReceived;
+ public event Action<WorkoutData> WillpowerDataReceived;
+ public event Action<WorkoutData> HrZoneDataReceived;
+ public event Action<WorkoutData> IntensityDataReceived;
+ public event Action<WorkoutData> AggregatesDataReceived;
+ public event Action<WorkoutData> CadenceDataReceived;
+ public event Action<WorkoutData> HrDataReceived;
+ public event Action<WorkoutData> SpeedDataReceived;
+ public event Action<WorkoutData> StrideLengthDataReceived;
+ public event Action<WorkoutData> FormCoachDataReceived;
+ public event Action<WorkoutData> GoalCoachDataReceived;
+ public event Action<WorkoutData> SpeedCoachDataReceived;
+
+ enum WorkoutStatus
+ {
+ NotStarted,
+ Preparing,
+ InProgress,
+ Paused,
+ Finished,
+ Saved
+ }
+
+ public Workout(IDispatcher dispatcher, IPermissionManager permissionManager)
+ {
+ _dispatcher = dispatcher;
+ _permissionManager = permissionManager;
+ }
+
+ public void PrepareWorkout(int activity, bool gpsNeeded, bool shoesNeeded, bool cadenceNeeded)
+ {
+ if (_isProgressing || _status != WorkoutStatus.NotStarted)
+ return;
+
+ _isProgressing = true;
+
+ //TODO : Create items
+ //TEST CODE
+ ClearAllWorkoutItem();
+ AddItem(WorkoutItemType.AvgHR);
+ AddItem(WorkoutItemType.PeakHR);
+ AddItem(WorkoutItemType.CurrentHR);
+ AddItem(WorkoutItemType.AvgPace);
+ AddItem(WorkoutItemType.MaxPace);
+ AddItem(WorkoutItemType.Pace);
+ AddItem(WorkoutItemType.AvgSpeed);
+ AddItem(WorkoutItemType.MaxSpeed);
+ AddItem(WorkoutItemType.Speed);
+ AddItem(WorkoutItemType.Cadence);
+ AddItem(WorkoutItemType.AvgCadence);
+ AddItem(WorkoutItemType.Distance);
+ AddItem(WorkoutItemType.Duration);
+ AddItem(WorkoutItemType.Calories);
+ AddItem(WorkoutItemType.HRZone);
+
+ _permissionManager.RequestPermission(PermissionType.Sensor, (isSensorSuccess, isSensorGranted) =>
+ {
+ if (!isSensorSuccess)
+ {
+ PrepareWorkout(false, false);
+ return;
+ }
+
+ if (!gpsNeeded)
+ {
+ PrepareWorkout(isSensorGranted, false);
+ return;
+ }
+
+ _permissionManager.RequestPermission(PermissionType.Location, (isGpsSuccess, isGpsGranted) =>
+ {
+ if (!isGpsSuccess)
+ {
+ PrepareWorkout(isSensorGranted, false);
+ return;
+ }
+
+ PrepareWorkout(isSensorGranted, isGpsGranted);
+ });
+ });
+ }
+
+ void PrepareWorkout(bool isSensorsGranted, bool isLocationGranted)
+ {
+ var prepareData = CreatDummyPrepareWorkoutData(isSensorsGranted, isLocationGranted);
+
+ var request = new Request(OperationType.PrepareWKO, prepareData);
+
+ request.ResponseReceived += (Response response) =>
+ {
+ Console.WriteLine($"Get PrepareWKO Response");
+ _status = WorkoutStatus.Preparing;
+ _isProgressing = false;
+ };
+ _dispatcher.SendRequest(request);
+ }
+
+ public void StartWorkout()
+ {
+ if (_isProgressing || _status != WorkoutStatus.Preparing)
+ return;
+
+ _isProgressing = true;
+
+ Console.WriteLine("Start Workout!");
+
+ var request = new Request(OperationType.StartWKO, "{\"activity_type\": 16}");
+ request.ResponseReceived += (Response response) =>
+ {
+ Console.WriteLine($"Get StartWKO Response");
+ _status = WorkoutStatus.InProgress;
+ _isProgressing = false;
+ };
+ _dispatcher.SendRequest(request);
+ }
+
+ public void FinishWorkout()
+ {
+ var request = new Request(OperationType.FinishWKO, "{\"activity_type\": 16}");
+ request.ResponseReceived += (Response response) =>
+ {
+ Console.WriteLine($"Get FinishWKO Response");
+ _status = WorkoutStatus.NotStarted;
+ _isListening = false;
+ _isProgressing = false;
+ };
+
+ _dispatcher.SendRequest(request);
+ }
+
+ public void StartListenWorkoutData()
+ {
+ if (_isListening)
+ return;
+
+ _isListening = true;
+
+ _workingDataObserver = new NotificationObserver(OperationType.WkoDataChanged);
+ _workingDataObserver.NotificationReceived += (string data) =>
+ {
+ WorkoutData workoutData = ParseWorkoutDataJson(data);
+
+ if (workoutData != null)
+ PropagateWorkoutData(workoutData);
+ };
+
+ var monitor = _dispatcher.Listen(OperationType.WkoDataChanged);
+ _workingDataObserver.Subscribe(monitor);
+ }
+
+ public WorkoutData ParseWorkoutDataJson(string jsonString)
+ {
+ WorkoutData workoutData = null;
+ try
+ {
+ workoutData = JsonConvert.DeserializeObject<WorkoutData>(jsonString);
+ }
+ catch
+ {
+ Console.WriteLine($"W.O DATA JSON Parse Fail: {jsonString}");
+ }
+ return workoutData;
+ }
+
+ public void PropagateWorkoutData(WorkoutData workoutData)
+ {
+ if (workoutData.calories != null)
+ {
+ CaloriesDataReceived?.Invoke(workoutData);
+ }
+ if (workoutData.willpower != null)
+ {
+ WillpowerDataReceived?.Invoke(workoutData);
+ }
+ if (workoutData.hrData != null)
+ {
+ HrDataReceived?.Invoke(workoutData);
+ }
+ if (workoutData.hrZone != null)
+ {
+ HrZoneDataReceived?.Invoke(workoutData);
+ }
+ if (workoutData.intensity != null)
+ {
+ IntensityDataReceived?.Invoke(workoutData);
+ }
+ if (workoutData.aggregates != null)
+ {
+ AggregatesDataReceived?.Invoke(workoutData);
+ }
+ if (workoutData.cadence != null)
+ {
+ CadenceDataReceived?.Invoke(workoutData);
+ }
+ if (workoutData.speed != null)
+ {
+ SpeedDataReceived?.Invoke(workoutData);
+ }
+ if (workoutData.strideLength != null)
+ {
+ StrideLengthDataReceived?.Invoke(workoutData);
+ }
+ if (workoutData.formCoach != null)
+ {
+ FormCoachDataReceived?.Invoke(workoutData);
+ }
+ if (workoutData.goalCoach != null)
+ {
+ GoalCoachDataReceived?.Invoke(workoutData);
+ }
+ if (workoutData.speedCoach != null)
+ {
+ SpeedCoachDataReceived?.Invoke(workoutData);
+ }
+ }
+
+ public void AddItem(WorkoutItemType type)
+ {
+ if (_workoutItems.ContainsKey((int)type))
+ return;
+
+ var item = WorkoutItemFactory.CreateWorkoutItem(type);
+
+ if (item == null)
+ return;
+
+ _workoutItems.Add((int)type, item);
+
+ SetWorkoutItemEvent(item, true);
+ }
+
+ void SetWorkoutItemEvent(IWorkoutItem item, bool on)
+ {
+ var type = item.GetWorkoutItemType();
+
+ switch (type)
+ {
+ case WorkoutItemType.Cadence:
+ if (on)
+ CadenceDataReceived += item.OnDataReceived;
+ else
+ CadenceDataReceived -= item.OnDataReceived;
+ break;
+ case WorkoutItemType.Speed:
+ case WorkoutItemType.Pace:
+ if (on)
+ SpeedDataReceived += item.OnDataReceived;
+ else
+ SpeedDataReceived -= item.OnDataReceived;
+ break;
+ case WorkoutItemType.Duration:
+ case WorkoutItemType.AvgCadence:
+ case WorkoutItemType.AvgPace:
+ case WorkoutItemType.MaxPace:
+ case WorkoutItemType.Distance:
+ case WorkoutItemType.AvgSpeed:
+ case WorkoutItemType.MaxSpeed:
+ if (on)
+ AggregatesDataReceived += item.OnDataReceived;
+ else
+ AggregatesDataReceived -= item.OnDataReceived;
+ break;
+ case WorkoutItemType.AvgHR:
+ case WorkoutItemType.CurrentHR:
+ case WorkoutItemType.PeakHR:
+ if (on)
+ HrDataReceived += item.OnDataReceived;
+ else
+ HrDataReceived -= item.OnDataReceived;
+ break;
+ case WorkoutItemType.Calories:
+ if (on)
+ CaloriesDataReceived += item.OnDataReceived;
+ else
+ CaloriesDataReceived -= item.OnDataReceived;
+ break;
+ case WorkoutItemType.HRZone:
+ if (on)
+ HrZoneDataReceived += item.OnDataReceived;
+ else
+ HrZoneDataReceived -= item.OnDataReceived;
+ break;
+ default:
+ break;
+ }
+ }
+
+ public IWorkoutItem GetItem(WorkoutItemType type)
+ {
+ if (_workoutItems.TryGetValue((int)type, out IWorkoutItem item))
+ return item;
+
+ return null;
+ }
+
+ public void ClearAllWorkoutItem()
+ {
+ foreach (var pair in _workoutItems)
+ {
+ SetWorkoutItemEvent(pair.Value, false);
+ }
+ _workoutItems.Clear();
+ }
+
+ string CreatDummyPrepareWorkoutData(bool isSensorsGranted, bool isLocationGranted)
+ {
+ // TODO : Implement with json parser
+ return "{\"id\":208,\"gps_needed\":false,\"shoes_needed\":false,\"cadence_needed\":true,\"distance_needed\":true,\"is_pace\":true,\"is_hr_needed\":true,\"form_coaching_type\":0}";
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRun.Models.Workout
+{
+ public class WorkoutData
+ {
+ public string calories { get; set; }
+ public string willpower { get; set; }
+ public string hrZone { get; set; }
+ public string intensity { get; set; }
+ public AggregateData aggregates { get; set; }
+ public ActivityData cadence { get; set; }
+ public ActivityData hrData { get; set; }
+ public ActivityData speed { get; set; }
+ public ActivityData strideLength { get; set; }
+ public RangeCoachingResult formCoach { get; set; }
+ public RangeCoachingResult speedCoach { get; set; }
+ public FitnessCoachingResult goalCoach { get; set; }
+ }
+
+ public class ActivityData
+ {
+ public float min { get; set; }
+ public float max { get; set; }
+ public float avg { get; set; }
+ public float latest { get; set; }
+ public string type { get; set; }
+ }
+
+ public class AggregateData
+ {
+ public float distance_total { get; set; }
+ public float metabolic_energy_total { get; set; }
+ public float intensity_avg { get; set; }
+ public float willpower { get; set; }
+ public int active_time_total { get; set; }
+ public int elapsed_time_total { get; set; }
+ public int steps_total { get; set; }
+ public int heartrate_avg { get; set; }
+ public int heartrate_max { get; set; }
+ public float speed_max { get; set; }
+ public float speed_avg { get; set; }
+ public float cadence_avg { get; set; }
+ public float cadence_max { get; set; }
+ public float stride_length_avg { get; set; }
+ }
+
+ public class RangeCoachingResult
+ {
+ public float targetMax { get; set; }
+ public float targetMin { get; set; }
+ public float currentValue { get; set; }
+ public int status { get; set; }
+ public bool notif { get; set; }
+ }
+
+ public class FitnessCoachingResult
+ {
+ public float current { get; set; }
+ public float target { get; set; }
+ public float percentage { get; set; }
+ }
+
+}
--- /dev/null
+using System;
+using NUnit.Framework;
+using Moq;
+using MapMyRunService.Models;
+using System.Collections.Generic;
+using MapMyRunService.Models.Account;
+using SQLite;
+
+namespace MapMyRunService.Tests
+{
+ [TestFixture]
+ class AccountManagerTest
+ {
+ IDB testDb = new DB();
+ Mock<IDispatcher> mockDispatcher = null;
+
+ public interface IDbConnection
+ {
+ int CreateTable();
+ }
+
+ [SetUp]
+ public void Setup()
+ {
+ mockDispatcher = new Mock<IDispatcher>();
+ }
+
+ [Test]
+ public void StartListenGetData_Called_CallDispatcherListen()
+ {
+ var accountManager = new AccountManager(testDb, mockDispatcher.Object);
+ mockDispatcher.Setup(x => x.Listen(OperationType.GetData)).Returns(new NotificationProvider(OperationType.GetData));
+ accountManager.StartListenGetData();
+
+ mockDispatcher.Verify(x => x.Listen(OperationType.GetData), Times.Once());
+ }
+
+ [Test]
+ public void StartListenGetData_ReceivedMessageWithUserProfileAction_CallSendReply()
+ {
+ var accountManager = new AccountManager(testDb, mockDispatcher.Object);
+ var notificationProvider = new NotificationProvider(OperationType.GetData);
+ Response receivedResponse = null;
+ mockDispatcher.Setup(x => x.Listen(OperationType.GetData)).Returns(notificationProvider);
+ mockDispatcher.Setup(x => x.SendReply(It.IsAny<Response>())).Callback<Response>(x => {
+ receivedResponse = x;
+ });
+ accountManager.StartListenGetData();
+
+ Dictionary<string, string> getDataUserProfile =
+ new Dictionary<string, string> { { "transID", "banana" }, { "data", "{\"action\": 1}" } };
+ notificationProvider.PostNotification(getDataUserProfile);
+ Assert.AreEqual(receivedResponse.Status, ResponseStatus.Success);
+ Assert.AreEqual(receivedResponse.TransID, "banana");
+
+ Dictionary<string, string> getDataUpdatedUserProfile =
+ new Dictionary<string, string> { { "transID", "orange" }, { "data", "{\"action\": 2}" } };
+ notificationProvider.PostNotification(getDataUpdatedUserProfile);
+ Assert.AreEqual(receivedResponse.Status, ResponseStatus.Success);
+ Assert.AreEqual(receivedResponse.TransID, "orange");
+
+ mockDispatcher.Verify(x => x.SendReply(It.IsAny<Response>()), Times.Exactly(2));
+ }
+
+ [Test]
+ public void StartListenGetData_ReceivedMessageWithAnotherAction_CallSendReply()
+ {
+ var accountManager = new AccountManager(testDb, mockDispatcher.Object);
+ var notificationProvider = new NotificationProvider(OperationType.GetData);
+ mockDispatcher.Setup(x => x.Listen(OperationType.GetData)).Returns(notificationProvider);
+ mockDispatcher.Setup(x => x.SendReply(It.IsAny<Response>()));
+ accountManager.StartListenGetData();
+
+ Dictionary<string, string> getDataUpdatedUserProfile =
+ new Dictionary<string, string> { { "transID", "orange" }, { "data", "{\"action\": 3}" } };
+ notificationProvider.PostNotification(getDataUpdatedUserProfile);
+ mockDispatcher.Verify(x => x.SendReply(It.IsAny<Response>()), Times.Never());
+ }
+ }
+}
+
+
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using MapMyRunService.Models;
+using SQLite;
+using SQLitePCL;
+
+namespace MapMyRunService.Tests
+{
+ public class DB : IDB
+ {
+ private SQLiteConnection dbConnection { get; set; }
+
+ private const string databaseFileName = "MMRServiceTestDB.db3";
+ private string databasePath;
+
+ public DB()
+ {
+ raw.SetProvider(new SQLite3Provider_sqlite3());
+ raw.FreezeProvider(true);
+
+ databasePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), databaseFileName);
+ dbConnection = new SQLiteConnection(databasePath);
+ }
+
+ public SQLiteConnection getConnection()
+ {
+ return dbConnection;
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+using System;
+using NUnit.Framework;
+using Moq;
+using MapMyRunService.Models;
+using System.Collections.Generic;
+
+namespace MapMyRunService.Tests
+{
+ [TestFixture]
+ class DispatcherTest
+ {
+ Mock<IMessagePortHandler> mockMessagePortHandler = null;
+
+ [SetUp]
+ public void Setup()
+ {
+ mockMessagePortHandler = new Mock<IMessagePortHandler>();
+ }
+
+ [Test]
+ public void Initialize_Called_CallMessagePortHandlerConnect()
+ {
+ mockMessagePortHandler.Setup(x => x.Connect(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>()));
+
+ var dispatcher = MapMyRunService.Models.Dispatcher.Instance;
+ dispatcher.Initialize(mockMessagePortHandler.Object);
+
+ mockMessagePortHandler.Verify(x => x.Connect(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>()), Times.Once());
+ }
+
+ [Test]
+ public void Initialize_Called_AddOnReceiveMessage()
+ {
+ var dispatcher = MapMyRunService.Models.Dispatcher.Instance;
+
+ mockMessagePortHandler.SetupAdd(m => m.MessageReceived += dispatcher.OnReceiveMessage);
+
+ dispatcher.Initialize(mockMessagePortHandler.Object);
+ mockMessagePortHandler.VerifyAdd(m => m.MessageReceived += dispatcher.OnReceiveMessage, Times.Once());
+ }
+
+ [Test]
+ public void SendRequest_FirstCalled_CallMessagePortHandlerSend()
+ {
+ var requestData = "banana";
+ var request = new Mock<Request>(OperationType.PrepareWKO, requestData);
+ Dictionary<string, string> requestDataSet = null;
+
+ mockMessagePortHandler.Setup(x => x.Send(It.IsAny<Dictionary<string, string>>()))
+ .Callback<Dictionary<string, string>>(arg => {
+ requestDataSet = arg;
+ }
+ );
+
+ var dispatcher = MapMyRunService.Models.Dispatcher.Instance;
+ dispatcher.Initialize(mockMessagePortHandler.Object);
+
+ dispatcher.SendRequest(request.Object);
+ requestDataSet.TryGetValue(Dispatcher.OperationKey, out string operation);
+ requestDataSet.TryGetValue(Dispatcher.DataKey, out string data);
+
+ mockMessagePortHandler.Verify(x => x.Send(It.IsAny<Dictionary<string, string>>()), Times.Once());
+ Assert.AreEqual(((int)OperationType.PrepareWKO).ToString(), operation);
+ Assert.AreEqual(requestData, data);
+ }
+
+ [Test]
+ public void OnReceiveMessage_Called_CallCorrectRequestOnReceiveResponse()
+ {
+ string requestData = "orange";
+ var request = new Request(OperationType.PrepareWKO, requestData);
+ bool isCalled = false;
+ request.ResponseReceived += (Response data) =>
+ {
+ isCalled = true;
+ };
+
+ var dispatcher = MapMyRunService.Models.Dispatcher.Instance;
+ dispatcher.Initialize(mockMessagePortHandler.Object);
+ var tid = dispatcher.SendRequest(request);
+
+ var responseData = new Dictionary<string, string>
+ {
+ { Dispatcher.TransIdKey, tid}
+ };
+
+ dispatcher.OnReceiveMessage(responseData);
+ Assert.IsTrue(isCalled);
+ }
+
+ [Test]
+ public void OnReceiveMessage_Called_DontCallIncorrectRequestOnReceiveResponse()
+ {
+ string requestData = "orange";
+ var request = new Request(OperationType.PrepareWKO, requestData);
+ var isCalled = false;
+ request.ResponseReceived += (Response data) =>
+ {
+ isCalled = true;
+ };
+
+ var dispatcher = MapMyRunService.Models.Dispatcher.Instance;
+ dispatcher.Initialize(mockMessagePortHandler.Object);
+ var tid = dispatcher.SendRequest(request);
+
+ var responseData = new Dictionary<string, string>
+ {
+ { Dispatcher.TransIdKey, tid + "Never"}
+ };
+
+ dispatcher.OnReceiveMessage(responseData);
+ Assert.IsFalse(isCalled);
+ }
+
+ [Test]
+ public void OnReceiveMessage_WithOperation_DontCallRequestOnReceiveResponse()
+ {
+ string requestData = "orange";
+ var request = new Request(OperationType.PrepareWKO, requestData);
+ var isCalled = false;
+ request.ResponseReceived += (Response data) =>
+ {
+ isCalled = true;
+ };
+
+ var dispatcher = MapMyRunService.Models.Dispatcher.Instance;
+ dispatcher.Initialize(mockMessagePortHandler.Object);
+ var tid = dispatcher.SendRequest(request);
+
+ var responseData = new Dictionary<string, string>
+ {
+ { Dispatcher.OperationKey, "kiwi" },
+ { Dispatcher.TransIdKey, tid}
+ };
+
+ dispatcher.OnReceiveMessage(responseData);
+ Assert.IsFalse(isCalled);
+ }
+
+ [Test]
+ public void OnReceiveMessage_WithoutOperation_ProvideCorrectNotification()
+ {
+ var isCalled = false;
+ var mockObserver = new NotificationObserver(OperationType.ApplicationInitialized);
+ mockObserver.NotificationReceived += (Dictionary<string, string> data) =>
+ {
+ isCalled = true;
+ };
+
+ var dispatcher = MapMyRunService.Models.Dispatcher.Instance;
+ dispatcher.Initialize(mockMessagePortHandler.Object);
+
+ var monitor = dispatcher.Listen(OperationType.ApplicationInitialized);
+ mockObserver.Subscribe(monitor);
+
+ Dictionary<string, string> responseData = new Dictionary<string, string>
+ {
+ { Dispatcher.TransIdKey, "orange"},
+ { Dispatcher.DataKey, "apple"},
+ { Dispatcher.OperationKey, "46"}
+ };
+ dispatcher.OnReceiveMessage(responseData);
+
+ Assert.IsTrue(isCalled);
+ }
+
+ [Test]
+ public void Listen_Called_ReturnCorrectNotificationProvider()
+ {
+ var dispatcher = MapMyRunService.Models.Dispatcher.Instance;
+ dispatcher.Initialize(mockMessagePortHandler.Object);
+ var notificationMonitor = dispatcher.Listen(OperationType.ApplicationInitialized);
+ Assert.AreEqual(notificationMonitor.Type, OperationType.ApplicationInitialized);
+ }
+
+ }
+}
--- /dev/null
+using System;
+using NUnit.Framework;
+using Moq;
+using MapMyRunService.Models;
+using MapMyRunService.Models.KeepAlive;
+using System.Collections.Generic;
+
+namespace MapMyRunService.Tests
+{
+ [TestFixture]
+ class KeepAliveTest
+ {
+ Mock<IDispatcher> mockDispatcher = null;
+
+ [SetUp]
+ public void Setup()
+ {
+ mockDispatcher = new Mock<IDispatcher>();
+ }
+
+ [Test]
+ public void ListenExitApp_Called_CallDispatcherListen()
+ {
+ var mockTizenServiceApplication = new Mock<ITizenServiceApplication>();
+ var keepAlive = new KeepAlive(mockDispatcher.Object, mockTizenServiceApplication.Object);
+ mockDispatcher.Setup(x => x.Listen(OperationType.ExitApp)).Returns(new NotificationProvider(OperationType.ExitApp));
+
+ keepAlive.Start();
+
+ mockDispatcher.Verify(x => x.Listen(OperationType.ExitApp), Times.Once());
+ }
+
+ [Test]
+ public void ListenExitApp_ReceivedMessage_CallExitAppication()
+ {
+ var mockTizenServiceApplication = new Mock<ITizenServiceApplication>();
+ mockTizenServiceApplication.Setup(x => x.ExitApplication());
+ var keepAlive = new KeepAlive(mockDispatcher.Object, mockTizenServiceApplication.Object);
+ var notificationProvider = new NotificationProvider(OperationType.ExitApp);
+ mockDispatcher.Setup(x => x.Listen(OperationType.ExitApp)).Returns(notificationProvider);
+ keepAlive.Start();
+ Dictionary<string, string> exit = new Dictionary<string, string> { { "data", "exit" } };
+ notificationProvider.PostNotification(exit);
+ mockTizenServiceApplication.Verify(x => x.ExitApplication(), Times.Once());
+ }
+
+ }
+}
+
+
--- /dev/null
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netcoreapp3.1</TargetFramework>
+
+ <IsPackable>false</IsPackable>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Moq" Version="4.13.1" />
+ <PackageReference Include="nunit" Version="3.12.0" />
+ <PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
+ <PackageReference Include="sqlite" Version="3.13.0" />
+ <PackageReference Include="sqlite-net-base" Version="1.5.166-beta" />
+ <PackageReference Include="SQLitePCLRaw.provider.sqlite3.netstandard11" Version="1.1.14" />
+ </ItemGroup>
+
+ <Import Project="..\MapMyRunService\MapMyRunService.projitems" Label="Shared" />
+
+</Project>
--- /dev/null
+<Project Sdk="Tizen.NET.Sdk/1.0.9">
+
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>tizen40</TargetFramework>
+ </PropertyGroup>
+
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugType>portable</DebugType>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>None</DebugType>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
+ <PackageReference Include="sqlite" Version="3.13.0" />
+ <PackageReference Include="sqlite-net-base" Version="1.5.166-beta" />
+ <PackageReference Include="SQLitePCLRaw.provider.sqlite3.netstandard11" Version="1.1.14" />
+ </ItemGroup>
+
+ <Import Project="..\MapMyRunService\MapMyRunService.projitems" Label="Shared" />
+
+</Project>
--- /dev/null
+using MapMyRunService.Models;
+using MapMyRunService.Models.Settings;
+using MapMyRunService.Models.KeepAlive;
+using MapMyRunService.Tizen.Models;
+using MapMyRunService.Tizen.Models.Settings;
+using MapMyRunService.Tizen.Models.Database;
+using MapMyRunService.Tizen.Services;
+using System;
+using Tizen.Applications;
+
+namespace MapMyRunService.Tizen
+{
+ class App : ServiceApplication
+ {
+ private string uiAppId = "org.tizen.example.MapMyRun";
+ private IMessagePortHandler messagePortHandler;
+ private Loading loading;
+ private KeepAlive keepAlive;
+ private SettingsManager setting;
+ private DB database;
+
+ protected override void OnCreate()
+ {
+ base.OnCreate();
+
+ WorkoutService.Instance.Initialize();
+ database = new DB();
+ messagePortHandler = new MessagePortHandler();
+ Dispatcher.Instance.Initialize(messagePortHandler);
+
+ setting = new SettingsManager(Dispatcher.Instance);
+ setting.initializeGPS(new GPSManager());
+ setting.Initialize();
+
+ loading = new Loading(messagePortHandler, uiAppId, database, new AuthCache());
+ loading.StartLoading();
+
+ keepAlive = new KeepAlive(Dispatcher.Instance, new TizenServiceApplication(this));
+ keepAlive.Start();
+
+ Console.WriteLine($"MapMyRunService application started.");
+ }
+
+ protected override void OnAppControlReceived(AppControlReceivedEventArgs e)
+ {
+ base.OnAppControlReceived(e);
+ }
+
+ protected override void OnDeviceOrientationChanged(DeviceOrientationEventArgs e)
+ {
+ base.OnDeviceOrientationChanged(e);
+ }
+
+ protected override void OnLocaleChanged(LocaleChangedEventArgs e)
+ {
+ base.OnLocaleChanged(e);
+ }
+
+ protected override void OnLowBattery(LowBatteryEventArgs e)
+ {
+ base.OnLowBattery(e);
+ }
+
+ protected override void OnLowMemory(LowMemoryEventArgs e)
+ {
+ base.OnLowMemory(e);
+ }
+
+ protected override void OnRegionFormatChanged(RegionFormatChangedEventArgs e)
+ {
+ base.OnRegionFormatChanged(e);
+ }
+
+ protected override void OnTerminate()
+ {
+ base.OnTerminate();
+ }
+
+ static void Main(string[] args)
+ {
+ App app = new App();
+ app.Run(args);
+ }
+ }
+}
--- /dev/null
+using MapMyRunService.Models.Auth;
+using System;
+using Tizen.Applications;
+
+namespace MapMyRunService.Tizen.Models
+{
+ public class AuthCache : IAuthCache
+ {
+ private readonly string CLIENT_ID_KEY = "token_ClientId";
+ private readonly string CLIENT_SECRET_KEY = "token_ClientSecret";
+ private readonly string ACCESS_TOKEN_KEY = "token_AccessToken";
+ private readonly string REFRESH_TOKEN_KEY = "token_RefreshToken";
+
+ public AuthCache()
+ {
+ }
+
+ public bool GetToken(AuthenticationToken authToken)
+ {
+ if (Preference.Contains(CLIENT_ID_KEY) && Preference.Get<string>(CLIENT_ID_KEY) == authToken.ClientId)
+ {
+
+ authToken.ClientSecret = Preference.Get<string>(CLIENT_SECRET_KEY);
+ authToken.AccessToken = Preference.Get<string>(ACCESS_TOKEN_KEY);
+ authToken.RefreshToken = Preference.Get<string>(REFRESH_TOKEN_KEY);
+
+ Console.WriteLine($"[AuthCache] [Get] Using token in cache.");
+ return true;
+ }
+
+ Console.WriteLine($"[AuthCache] No cached token found.");
+
+ return false;
+ }
+
+ public void SaveToken(AuthenticationToken authToken)
+ {
+ Preference.Set(CLIENT_ID_KEY, authToken.ClientId);
+ Preference.Set(CLIENT_SECRET_KEY, authToken.ClientSecret);
+ Preference.Set(ACCESS_TOKEN_KEY, authToken.AccessToken);
+ Preference.Set(REFRESH_TOKEN_KEY, authToken.RefreshToken);
+
+ Console.WriteLine($"[AuthCache] Update token in cache:");
+ logTokenCache();
+ }
+
+ public void RemoveToken()
+ {
+ Console.WriteLine($"[AuthCache] RemoveToken called.");
+ logTokenCache();
+
+ if (Preference.Contains(CLIENT_ID_KEY))
+ {
+ Preference.Remove(CLIENT_ID_KEY);
+ Preference.Remove(CLIENT_SECRET_KEY);
+ Preference.Remove(ACCESS_TOKEN_KEY);
+ Preference.Remove(REFRESH_TOKEN_KEY);
+
+ Console.WriteLine($"[AuthCache] [Remove] Success: {!Preference.Contains(CLIENT_ID_KEY)}");
+ }
+ }
+
+ private void logTokenCache()
+ {
+ if (Preference.Contains(CLIENT_ID_KEY))
+ {
+ Console.WriteLine($"[AuthCache] ClientId: {Preference.Get<string>(CLIENT_ID_KEY)}");
+ Console.WriteLine($"[AuthCache] ClientSecret: {Preference.Get<string>(CLIENT_SECRET_KEY)}");
+ Console.WriteLine($"[AuthCache] AccessToken: {Preference.Get<string>(ACCESS_TOKEN_KEY)}");
+ Console.WriteLine($"[AuthCache] RefreshToken: {Preference.Get<string>(REFRESH_TOKEN_KEY)}");
+ } else
+ {
+ Console.WriteLine($"[AuthCache] No cached token.");
+ }
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using MapMyRunService.Models;
+using SQLite;
+using SQLitePCL;
+using Tizen.Applications;
+
+namespace MapMyRunService.Tizen.Models.Database
+{
+ public class DB : IDB
+ {
+ private SQLiteConnection dbConnection { get; set; }
+
+ private const string databaseFileName = "MMRServiceDB.db3";
+ private string databasePath;
+
+ public DB()
+ {
+ Console.WriteLine($"[DB] Initialize DB");
+
+ raw.SetProvider(new SQLite3Provider_sqlite3());
+ raw.FreezeProvider(true);
+
+ databasePath = Path.Combine(Application.Current.DirectoryInfo.Data, databaseFileName);
+ dbConnection = new SQLiteConnection(databasePath);
+ }
+
+ public SQLiteConnection getConnection()
+ {
+ return dbConnection;
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+using Tizen.Applications;
+using Tizen.Applications.Messages;
+using MapMyRunService.Models;
+using ElmSharp;
+
+namespace MapMyRunService.Tizen.Models
+{
+ public class MessagePortHandler : IMessagePortHandler
+ {
+ private string _localPortName = "";
+ private string _remotePortName = "";
+ private string _remoteAppId = "";
+ private bool _isSecureMode = false;
+
+ private bool _isInit = false;
+ private bool _isPendingMessageSending = false;
+ private List<Dictionary<string, string>> _pendingMessages = new List<Dictionary<string, string>>();
+
+ private MessagePort messagePort;
+
+ public event Action<Dictionary<string, string>> MessageReceived = delegate { };
+
+ public void Connect(string localPort, string remoteAppId, string remotePort, bool secureMode)
+ {
+ _localPortName = localPort;
+ _remotePortName = remotePort;
+ _remoteAppId = remoteAppId;
+ _isSecureMode = secureMode;
+
+ messagePort = new MessagePort(_localPortName, _isSecureMode);
+ messagePort.MessageReceived += OnReceive;
+ messagePort.Listen();
+ }
+
+ public void Disconnect()
+ {
+ _localPortName = "";
+ _remotePortName = "";
+ _remoteAppId = "";
+ _isSecureMode = false;
+ messagePort.StopListening();
+ messagePort.Dispose();
+ messagePort = null;
+ }
+
+ public void Send(Dictionary<string, string> data)
+ {
+ if (!_isInit)
+ {
+ _pendingMessages.Add(data);
+
+ if (!_isPendingMessageSending)
+ {
+ _isPendingMessageSending = true;
+ TrySendFirstMessage();
+ }
+ return;
+ }
+
+ SendMessage(data);
+ }
+
+ public bool IsRemoteRunning(string remoteAppId, string remotePortName, bool secureMode)
+ {
+ try
+ {
+ var remotePort = new RemotePort(remoteAppId, remotePortName, secureMode);
+ bool isRunning = remotePort.IsRunning();
+ Console.WriteLine($"Remote Port: {remoteAppId}, {remotePortName}, {isRunning}");
+
+ return isRunning;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine("Exception - Message: " + ex.Message + ", source: " + ex.Source + ", " + ex.StackTrace);
+ return false;
+ }
+ }
+
+ private void TrySendFirstMessage()
+ {
+ try
+ {
+ var remotePort = new RemotePort(_remoteAppId, _remotePortName, false);
+ bool isRunning = remotePort.IsRunning();
+ Console.WriteLine($"Remote Port: {_remoteAppId}, {_remotePortName}, {isRunning}");
+
+ if (!isRunning)
+ {
+ StartRetrySendFirstMessage();
+ return;
+ }
+
+ foreach (Dictionary<string, string> pendingMessage in _pendingMessages)
+ {
+ //TODO : Fail case, Exit? or Recovery?
+ SendMessage(pendingMessage);
+ }
+
+ _isPendingMessageSending = false;
+ _isInit = true;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine("Exception - Message: " + ex.Message + ", source: " + ex.Source + ", " + ex.StackTrace);
+ StartRetrySendFirstMessage();
+ }
+ }
+
+ void SendMessage(Dictionary<string, string> data)
+ {
+ var bundle = new Bundle();
+ Console.WriteLine($"________________________________");
+ Console.WriteLine($"[Service] >>>>> Send Data: ");
+ foreach (var pair in data)
+ {
+ bundle.AddItem(pair.Key, pair.Value);
+ Console.WriteLine($"{{ Key: {pair.Key}, Value: {pair.Value} }}");
+ }
+
+ try
+ {
+ messagePort.Send(bundle, _remoteAppId, _remotePortName, _isSecureMode);
+ Console.WriteLine($"Message sent: {_remoteAppId}, {_remotePortName}, {_isSecureMode}");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine("Exception - Message: " + ex.Message + ", source: " + ex.Source + ", " + ex.StackTrace);
+ }
+ }
+
+ private void StartRetrySendFirstMessage()
+ {
+ EcoreMainloop.AddTimer(0.5, () =>
+ {
+ TrySendFirstMessage();
+ return false;
+ });
+ }
+
+ private void OnReceive(object sender, MessageReceivedEventArgs e)
+ {
+ StringBuilder messageLog = new StringBuilder();
+ Console.WriteLine($"________________________________");
+ Console.WriteLine("[Service] Service application received a message");
+ Console.WriteLine($"App ID: {e.Remote.AppId}");
+ Console.WriteLine($"PortName: {e.Remote.PortName}");
+ Console.WriteLine($"Trusted: {e.Remote.Trusted}");
+ Bundle responseBundle = e.Message;
+
+ Console.WriteLine($"[Service] Response Data <<<<< : ");
+ var response = new Dictionary<string, string>();
+
+ foreach (string key in responseBundle.Keys)
+ {
+ if (responseBundle.TryGetItem(key, out string value))
+ {
+ Console.WriteLine($"{{ Key: {key}, Value: {value} }}");
+ response.Add(key, value);
+ }
+ }
+
+ MessageReceived(response);
+ }
+ }
+}
--- /dev/null
+using MapMyRunService.Models;
+using MapMyRunService.Models.Settings;
+using System;
+using System.Collections.Generic;
+using Tizen.Location;
+
+namespace MapMyRunService.Tizen.Models.Settings
+{
+ public class GPSManager : IGPSManager
+ {
+ public event EventHandler<GPSStateEventArgs> GPSChanged;
+ private Locator _locator;
+
+ public GPSManager()
+ {
+ _locator = new Locator(LocationType.Gps);
+ _locator.ServiceStateChanged += (object s, ServiceStateChangedEventArgs e) =>
+ {
+ GPSStateEventArgs args = new GPSStateEventArgs();
+ if (e.ServiceState == ServiceState.Enabled) args.enabled = true;
+ GPSChanged?.Invoke(this, args);
+ };
+ }
+ ~GPSManager()
+ {
+ _locator.Dispose();
+ }
+ public void DisableGps()
+ {
+ _locator.Stop();
+ }
+
+ public void EnableGps()
+ {
+ _locator.Start();
+ }
+ }
+}
--- /dev/null
+using MapMyRunService.Models;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Tizen.Applications;
+
+namespace MapMyRunService.Tizen.Models
+{
+ class TizenServiceApplication : ITizenServiceApplication
+ {
+ readonly ServiceApplication _serviceApp;
+
+ public TizenServiceApplication(ServiceApplication serviceApp)
+ {
+ _serviceApp = serviceApp;
+ }
+
+ public void ExitApplication()
+ {
+ _serviceApp.Exit();
+ }
+ }
+}
--- /dev/null
+using MapMyRunService.Models;
+using MapMyRunService.Tizen.Models;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Tizen.Location;
+using Tizen.Security;
+using Tizen.Sensor;
+
+namespace MapMyRunService.Tizen.Services
+{
+ public class SensorService
+ {
+ private readonly string healthSensorPrivilege = "http://tizen.org/privilege/healthinfo";
+ private readonly string gpsPrivilege = "http://tizen.org/privilege/location";
+
+ private HeartRateMonitor heartRateMonitor;
+ private Pedometer pedometerSensor;
+
+ private Locator locator;
+ private bool locatorRunning = false;
+
+ private bool runHealthRequested = false;
+ private bool runGpsRequested = false;
+
+
+ public HeartRateData CurrentHeartRateData { get; private set; }
+ public PedometerData CurrentPedometerData { get; private set; }
+ public LocationData CurrentLocationData { get; private set; }
+
+
+ private static SensorService _instance;
+ public static SensorService Instance
+ {
+ get
+ {
+ if (_instance == null)
+ _instance = new SensorService();
+
+ return _instance;
+ }
+ set
+ {
+ _instance = value;
+ }
+ }
+
+ private SensorService()
+ {
+ CurrentHeartRateData = new HeartRateData();
+ CurrentPedometerData = new PedometerData();
+ CurrentLocationData = new LocationData();
+
+ CheckPermissionsThenInit(healthSensorPrivilege);
+ CheckPermissionsThenInit(gpsPrivilege);
+ }
+
+ /// <summary>
+ /// Check sensor status
+ /// </summary>
+ /// <param name="sensor"></param>
+ /// <returns></returns>
+ public bool IsSensorRunning(SupportedSensor sensor)
+ {
+ switch (sensor)
+ {
+ case SupportedSensor.GPS:
+ return locator != null && locatorRunning;
+ case SupportedSensor.HeartRate:
+ return heartRateMonitor != null && heartRateMonitor.IsSensing;
+ case SupportedSensor.Pedometer:
+ return pedometerSensor != null && pedometerSensor.IsSensing;
+ }
+
+ return false;
+ }
+
+ private void InitializeHealthSensors()
+ {
+ Console.WriteLine($"Initialize Health Sensors (HR, Pedometer).");
+
+ if (heartRateMonitor == null && HeartRateMonitor.IsSupported)
+ {
+ heartRateMonitor = new HeartRateMonitor();
+ heartRateMonitor.DataUpdated += new EventHandler<HeartRateMonitorDataUpdatedEventArgs>(UpdateHeartRateData);
+ }
+
+ if (pedometerSensor == null && Pedometer.IsSupported)
+ {
+ pedometerSensor = new Pedometer();
+ pedometerSensor.DataUpdated += new EventHandler<PedometerDataUpdatedEventArgs>(UpdatePedometerData);
+ }
+
+ if (runHealthRequested)
+ {
+ RunHealthSensors();
+ }
+ }
+
+ private void InitializeGPS()
+ {
+ Console.WriteLine($"Initialize GPS.");
+ if (locator == null)
+ {
+ locator = new Locator(LocationType.Gps);
+ locator.LocationChanged += new EventHandler<LocationChangedEventArgs>(UpdateLocationData);
+
+ if (runGpsRequested)
+ {
+ RunGPS();
+ }
+ }
+ }
+
+ public void RunHealthSensors()
+ {
+ if (heartRateMonitor != null && !heartRateMonitor.IsSensing)
+ {
+ heartRateMonitor.Start();
+ Console.WriteLine($"HR Monitor started.");
+ }
+
+ if (pedometerSensor != null && !pedometerSensor.IsSensing)
+ {
+ pedometerSensor.Start();
+ Console.WriteLine($"Pedometer started.");
+ }
+
+ // Unable to run health sensors since they are not yet initialized
+ if ((heartRateMonitor == null && pedometerSensor == null))
+ {
+ runHealthRequested = true;
+ } else
+ {
+ runHealthRequested = false;
+ }
+ }
+
+ public void RunGPS()
+ {
+ if (locator != null)
+ {
+ locator.Start();
+ locatorRunning = true;
+ runGpsRequested = false;
+
+ Console.WriteLine($"GPS Sensing started.");
+ } else
+ {
+ // unable to run gps despite request
+ runGpsRequested = true;
+ }
+ }
+
+ public void StopSensor(SupportedSensor supportedSensor)
+ {
+ switch (supportedSensor)
+ {
+ case SupportedSensor.HeartRate:
+ if (HeartRateMonitor.IsSupported && heartRateMonitor.IsSensing)
+ {
+ heartRateMonitor.Stop();
+ Console.WriteLine($"HR Monitor stopped.");
+ }
+ break;
+ case SupportedSensor.Pedometer:
+ if (Pedometer.IsSupported && pedometerSensor.IsSensing)
+ {
+ pedometerSensor.Stop();
+ Console.WriteLine($"Pedometer stopped.");
+ }
+ break;
+ case SupportedSensor.GPS:
+ if (locatorRunning)
+ {
+ locatorRunning = false;
+ locator?.Stop();
+ locator?.Dispose();
+ Console.WriteLine($"GPS sensing stopped.");
+ }
+ break;
+ case SupportedSensor.All:
+ if (HeartRateMonitor.IsSupported && heartRateMonitor.IsSensing)
+ {
+ heartRateMonitor.Stop();
+ Console.WriteLine($"HR Monitor stopped.");
+ }
+ if (Pedometer.IsSupported && pedometerSensor.IsSensing)
+ {
+ pedometerSensor.Stop();
+ Console.WriteLine($"Pedometer stopped.");
+ }
+ if (locatorRunning)
+ {
+ locatorRunning = false;
+ locator?.Stop();
+ // locator?.Dispose();
+ Console.WriteLine($"GPS sensing stopped.");
+ }
+ break;
+ }
+ }
+
+ private void UpdatePedometerData(object sender, PedometerDataUpdatedEventArgs e)
+ {
+ //if (CurrentPedometerData.StepCount != e.StepCount)
+ // Console.WriteLine($"Updated step count: {CurrentPedometerData.StepCount}");
+
+ CurrentPedometerData.StepCount = e.StepCount;
+ CurrentPedometerData.Distance = e.MovingDistance;
+ CurrentPedometerData.CaloriesBurned = e.CalorieBurned;
+ CurrentPedometerData.RunStepCount = e.RunStepCount;
+ CurrentPedometerData.WalkStepCount = e.WalkStepCount;
+ CurrentPedometerData.Speed = e.LastSpeed;
+ }
+
+ private void UpdateHeartRateData(object sender, HeartRateMonitorDataUpdatedEventArgs e)
+ {
+ //if (CurrentHeartRateData.LastHeartRate != e.HeartRate)
+ // Console.WriteLine($"Updated HR: {CurrentHeartRateData.LastHeartRate}, Min: {CurrentHeartRateData.MinHeartRate}, Max: {CurrentHeartRateData.MaxHeartRate}");
+
+ CurrentHeartRateData.LastHeartRate = e.HeartRate;
+ }
+
+ private void UpdateLocationData(object sender, LocationChangedEventArgs e)
+ {
+ //if (CurrentLocationData.Latitude != e.Location.Latitude || CurrentLocationData.Longitude != e.Location.Longitude)
+ // Console.WriteLine($"Updated location: Lat: {e.Location.Latitude}, Long: {e.Location.Longitude}");
+
+
+ CurrentLocationData.Latitude = e.Location.Latitude;
+ CurrentLocationData.Longitude = e.Location.Longitude;
+ }
+
+ private void CheckPermissionsThenInit(string privilege)
+ {
+ CheckResult result = PrivacyPrivilegeManager.CheckPermission(privilege);
+
+ switch (result)
+ {
+ case CheckResult.Allow:
+ Console.WriteLine($"{privilege}: Allow");
+ if (privilege == healthSensorPrivilege)
+ InitializeHealthSensors();
+ else
+ InitializeGPS();
+ break;
+ case CheckResult.Ask:
+ Console.WriteLine($"{privilege}: Ask");
+
+ PrivacyPrivilegeManager.RequestPermission(privilege);
+ break;
+ case CheckResult.Deny:
+ Console.WriteLine($"{privilege}: Deny");
+
+ throw new UnauthorizedAccessException($"Service has no permission to use {privilege}");
+ }
+
+ PrivacyPrivilegeManager.ResponseContext responseContext;
+ PrivacyPrivilegeManager.GetResponseContext(privilege).TryGetTarget(out responseContext);
+ responseContext.ResponseFetched += new EventHandler<RequestResponseEventArgs>(PermissionUpdated);
+ }
+
+
+ void PermissionUpdated(object sender, RequestResponseEventArgs e)
+ {
+ Console.WriteLine($"Privilege {e.privilege} updated to: {e.result}");
+ CheckPermissionsThenInit(e.privilege);
+ }
+ }
+
+ public enum SupportedSensor
+ {
+ HeartRate,
+ Pedometer,
+ GPS,
+ All
+ }
+}
--- /dev/null
+using MapMyRunService.Models;
+using MapMyRunService.Models.Workout;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace MapMyRunService.Tizen.Services
+{
+ public class WorkoutService
+ {
+ private WorkoutManager workoutManager;
+
+ private SensorService sensorService;
+
+ public static WorkoutService Instance { get; private set; } = new WorkoutService();
+ private WorkoutService()
+ {
+ sensorService = SensorService.Instance;
+ workoutManager = WorkoutManager.Instance;
+ }
+
+ public void Initialize()
+ {
+ workoutManager.PrepareWKO += PrepareWorkout;
+ workoutManager.StartWKO += StartWorkout;
+ workoutManager.PauseWKO += PauseWorkout;
+ workoutManager.ResumeWKO += ResumeWorkout;
+ workoutManager.FinishWKO += EndWorkout;
+ workoutManager.ReadData += UpdateWorkoutData;
+
+ Console.WriteLine($"Workout service initialized.");
+ }
+
+ public void PrepareWorkout(WorkoutReport workoutReport)
+ {
+ Console.WriteLine($"[WorkoutService] Workout Prepared");
+
+ workoutReport.LocStart.Latitude = sensorService.CurrentLocationData.Latitude;
+ workoutReport.LocStart.Longitude = sensorService.CurrentLocationData.Longitude;
+ }
+
+ public void StartWorkout(WorkoutReport workoutReport)
+ {
+ Console.WriteLine($"[WorkoutService] Workout Started");
+
+ workoutReport.StartTime = DateTime.UtcNow;
+
+ sensorService.RunHealthSensors();
+ sensorService.RunGPS();
+ }
+
+ public void EndWorkout(WorkoutReport workoutReport)
+ {
+ Console.WriteLine($"[WorkoutService] Workout Finished");
+
+ workoutReport.EndTime = DateTime.UtcNow;
+ workoutReport.LocEnd.Latitude = sensorService.CurrentLocationData.Latitude;
+ workoutReport.LocEnd.Longitude = sensorService.CurrentLocationData.Longitude;
+
+ sensorService.StopSensor(SupportedSensor.All);
+ }
+
+ public void PauseWorkout(WorkoutReport workoutReport)
+ {
+ Console.WriteLine($"[WorkoutService] Workout Paused");
+ sensorService.StopSensor(SupportedSensor.All);
+ }
+
+ public void ResumeWorkout(WorkoutReport workoutReport)
+ {
+ Console.WriteLine($"[WorkoutService] Workout Resumed");
+
+ sensorService.RunHealthSensors();
+ sensorService.RunGPS();
+ }
+
+ public void UpdateWorkoutData(WorkoutReport workoutReport)
+ {
+ // Package workout data here
+
+ WorkoutData workoutData = new WorkoutData();
+ workoutData.calories = sensorService.CurrentPedometerData.CaloriesBurned.ToString();
+
+ AggregateData aggregateData = new AggregateData();
+ aggregateData.distance_total = sensorService.CurrentPedometerData.Distance;
+ aggregateData.steps_total = (int) sensorService.CurrentPedometerData.StepCount;
+ aggregateData.heartrate_avg = sensorService.CurrentHeartRateData.AvgHeartRate;
+ aggregateData.heartrate_max = sensorService.CurrentHeartRateData.MaxHeartRate;
+
+ workoutData.aggregates = aggregateData;
+
+ ActivityData hrActivity = new ActivityData();
+ hrActivity.min = sensorService.CurrentHeartRateData.MinHeartRate;
+ hrActivity.max = sensorService.CurrentHeartRateData.MaxHeartRate;
+ hrActivity.avg = sensorService.CurrentHeartRateData.AvgHeartRate;
+ hrActivity.latest = sensorService.CurrentHeartRateData.LastHeartRate;
+
+ workoutReport.workoutData = workoutData;
+ workoutReport.workoutData.hrData = hrActivity;
+ }
+
+ }
+}
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<manifest package="org.tizen.example.MapMyRunService" version="1.0.0" api-version="4" xmlns="http://tizen.org/ns/packages">
+ <profile name="wearable" />
+ <service-application appid="org.tizen.example.MapMyRunService" exec="MapMyRunService.Tizen.dll" multiple="false" nodisplay="true" taskmanage="false" type="dotnet">
+ <label>MapMyRunService</label>
+ <icon>MapMyRunService.png</icon>
+ <splash-screens />
+ </service-application>
+ <shortcut-list />
+ <privileges>
+ <privilege>http://tizen.org/privilege/healthinfo</privilege>
+ <privilege>http://tizen.org/privilege/location</privilege>
+ </privileges>
+ <dependencies />
+ <provides-appdefined-privileges />
+ <feature name="http://tizen.org/feature/sensor.pedometer">true</feature>
+ <feature name="http://tizen.org/feature/sensor.heart_rate_monitor">true</feature>
+ <feature name="http://tizen.org/feature/location.gps">true</feature>
+</manifest>
--- /dev/null
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.29519.87
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MapMyRunService.Tizen", "MapMyRunService.Tizen\MapMyRunService.Tizen.csproj", "{DACD098D-6BEB-491C-A91D-4FABB3D4490A}"
+EndProject
+Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "MapMyRunService", "MapMyRunService\MapMyRunService.shproj", "{7B89FD19-6DCD-4721-9000-FA612161C492}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MapMyRunService.Tests", "MapMyRunService.Test\MapMyRunService.Tests.csproj", "{D8AF456A-10C8-4BFE-BB6B-0120221A071A}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {DACD098D-6BEB-491C-A91D-4FABB3D4490A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DACD098D-6BEB-491C-A91D-4FABB3D4490A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DACD098D-6BEB-491C-A91D-4FABB3D4490A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DACD098D-6BEB-491C-A91D-4FABB3D4490A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D8AF456A-10C8-4BFE-BB6B-0120221A071A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D8AF456A-10C8-4BFE-BB6B-0120221A071A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D8AF456A-10C8-4BFE-BB6B-0120221A071A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D8AF456A-10C8-4BFE-BB6B-0120221A071A}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {C7B6B376-8277-40AC-83A5-4D7204FE8B38}
+ EndGlobalSection
+EndGlobal
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <PropertyGroup>
+ <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
+ <HasSharedItems>true</HasSharedItems>
+ <SharedGUID>7b89fd19-6dcd-4721-9000-fa612161c492</SharedGUID>
+ </PropertyGroup>
+ <PropertyGroup Label="Configuration">
+ <Import_RootNamespace>MapMyRunService</Import_RootNamespace>
+ </PropertyGroup>
+ <ItemGroup>
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Account\User.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Account\UserDao.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Auth\IAuthCache.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Auth\IAuthenticationProvider.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Auth\AuthenticationToken.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Auth\Authentication.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Auth\ExampleAuthProvider.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\HttpService.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Dispatcher.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\IDB.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\IDispatcher.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\IMessagePortHandler.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\KeepAlive\KeepAlive.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Loading.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\NotificationObserver.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\NotificationProvider.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Request.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Response.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\ITizenServiceApplication.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Settings\SettingsManager.cs" />
+ <Compile Include="..\MapMyRunService\Models\Settings\IGPSManager.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Settings\SettingsData.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Account\AccountManager.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Workout\WorkoutManager.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Workout\HeartRateData.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Workout\LocationData.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Workout\PedometerData.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)Models\Workout\WorkoutData.cs" />
+ <Compile Include="..\MapMyRunService\Models\Workout\WorkoutReport.cs" />
+ </ItemGroup>
+</Project>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <PropertyGroup Label="Globals">
+ <ProjectGuid>7b89fd19-6dcd-4721-9000-fa612161c492</ProjectGuid>
+ <MinimumVisualStudioVersion>14.0</MinimumVisualStudioVersion>
+ </PropertyGroup>
+ <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+ <Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props" />
+ <Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props" />
+ <PropertyGroup />
+ <Import Project="MapMyRunService.projitems" Label="Shared" />
+ <Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" />
+</Project>
--- /dev/null
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRunService.Models.Account
+{
+ public class AccountManager
+ {
+
+ private readonly IDispatcher _dispatcher;
+ private readonly IDB database;
+ private readonly UserDao userDao;
+ private const string GET_DATA_ACTION_KEY = "action";
+ private User currentUser;
+
+ private class GetDataContent
+ {
+ public int action;
+ }
+
+ public AccountManager(IDB _database, IDispatcher dispatcher)
+ {
+ database = _database;
+ userDao = new UserDao(database);
+ _dispatcher = dispatcher;
+ currentUser = userDao.Get("");
+ }
+
+ public void StartListenGetData()
+ {
+ NotificationObserver getDataObserver = new NotificationObserver(OperationType.GetData);
+ getDataObserver.NotificationReceived += (Dictionary<string, string> data) =>
+ {
+ data.TryGetValue("transID", out string transID);
+ data.TryGetValue("data", out string dataValue);
+
+ Console.WriteLine($"[Service] OperationType.GetData notification");
+ Console.WriteLine($"transID: {transID}, value: {dataValue}");
+
+ GetDataContent getDataContent = null;
+ try
+ {
+ getDataContent = JsonConvert.DeserializeObject<GetDataContent>(dataValue);
+ }
+ catch
+ {
+ Console.WriteLine("Get Data JSON Parse Fail");
+ }
+
+ if (DataActionType.GetUserProfile != (DataActionType)getDataContent.action
+ && DataActionType.GetUpdatedUserProfile != (DataActionType)getDataContent.action)
+ {
+ return;
+ }
+
+ string dataJson = JsonConvert.SerializeObject(currentUser);
+ var response = new Response(transID, dataJson, ResponseStatus.Success);
+
+ _dispatcher.SendReply(response);
+ };
+
+ var monitor = _dispatcher.Listen(OperationType.GetData);
+ getDataObserver.Subscribe(monitor);
+ }
+
+ public void DeleteCurrentUser()
+ {
+ currentUser = userDao.Get("");
+
+ if (currentUser != null)
+ {
+ Console.WriteLine($"[AccountManager] Deleting user");
+ userDao.Delete(currentUser);
+ }
+ currentUser = null;
+ }
+
+ public void UpdateUser(User user)
+ {
+ currentUser = user;
+ userDao.Update(user);
+ }
+
+ public void ChangeUser(User user)
+ {
+ DeleteCurrentUser();
+ userDao.Insert(user);
+ currentUser = user;
+ }
+ }
+}
--- /dev/null
+using SQLite;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRunService.Models.Account
+{
+ public class User
+ {
+ [PrimaryKey]
+ public int id { get; set; }
+ public string first_name { get; set; }
+ public string last_name { get; set; }
+ public string display_name { get; set; }
+ public double weight { get; set; }
+ public double height { get; set; }
+ public string hobbies { get; set; }
+ public string date_joined { get; set; }
+ public string introduction { get; set; }
+ public string display_measurement_system { get; set; }
+ public string last_login { get; set; }
+ public string email { get; set; }
+ public string username { get; set; }
+ public string last_initial { get; set; }
+ public string preferred_language { get; set; }
+ public string gender { get; set; }
+ public string time_zone { get; set; }
+ public string birthdate { get; set; }
+ public string profile_statement { get; set; }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRunService.Models.Account
+{
+ public class UserDao
+ {
+ private IDB database;
+ private readonly int SAMPLE_ID = 12345678;
+
+ public UserDao(IDB _database)
+ {
+ database = _database;
+ database.getConnection().CreateTable<User>();
+
+ InsertSampleData(); // temporary
+
+ Console.WriteLine($"[UserDao] Test DB Read:");
+ var user = database.getConnection().Table<User>().First();
+ Console.WriteLine($"[UserDao] Username: {user.username}, email: {user.email}");
+ }
+
+ public User Get(string id)
+ {
+ if (id.Equals(""))
+ {
+ try
+ {
+ var currentUser = database.getConnection().Table<User>().First();
+ return currentUser;
+ }
+ catch (Exception)
+ {
+ Console.WriteLine($"[DB] Get Error: no user found");
+ return null;
+ }
+ }
+
+ try
+ {
+ var currentUser = database.getConnection().Query<User>("select * from User where id = ?", id);
+ Console.WriteLine($"[DB] Get User: user size {currentUser.Count}");
+ return currentUser[0];
+ }
+ catch (Exception)
+ {
+ Console.WriteLine($"[DB] Get Error: no user found");
+ return null;
+ }
+ }
+
+ public void Insert(User user)
+ {
+ try
+ {
+ database.getConnection().Insert(user);
+ }
+ catch (Exception)
+ {
+ Console.WriteLine($"[DB] Get Error");
+ }
+ }
+
+ public void Update(User user)
+ {
+ try
+ {
+ database.getConnection().Update(user);
+ }
+ catch (Exception)
+ {
+ Console.WriteLine($"[DB] Update Error");
+ }
+ }
+
+ public void Delete(User user)
+ {
+ try
+ {
+ database.getConnection().Delete(user);
+ }
+ catch (Exception)
+ {
+ Console.WriteLine($"[DB] Delete Error: no user found");
+ }
+ }
+
+ public void InsertSampleData()
+ {
+ if (database.getConnection().Table<User>().Count() == 0)
+ {
+ Console.WriteLine($"DB is empty. Adding sample data.");
+
+ // add sample user
+ User sampleUser = new User
+ {
+ id = SAMPLE_ID,
+ first_name = "Sample",
+ last_name = "User",
+ display_name = "Sample",
+ weight = 75.55,
+ height = 1.75,
+ date_joined = "2019-09-20T04:51:23+00:00",
+ display_measurement_system = "metric",
+ last_login = "2019-09-20T04:51:23+00:00",
+ email = "sample_user@email.org",
+ username = "sample_user",
+ last_initial = "U",
+ preferred_language = "en-US",
+ gender = "M",
+ time_zone = "Asia/Seoul",
+ birthdate = "1940-02-25"
+ };
+
+ Insert(sampleUser);
+ }
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MapMyRunService.Models.Auth
+{
+ public class Authentication
+ {
+ private static SemaphoreSlim RequestLock = new SemaphoreSlim(1, 1);
+ private const string ClientId = "ExampleClientID123";
+ private const string ClientSecret = "ExampleClientSecret123";
+
+ private readonly IAuthenticationProvider authProvider;
+ private IAuthCache authCache;
+
+ public AuthenticationToken AuthToken { get; set; } = null;
+
+ public Authentication(IAuthenticationProvider authProvider, IAuthCache authCache)
+ {
+ AuthToken = new AuthenticationToken()
+ {
+ ClientId = ClientId,
+ ClientSecret = ClientSecret
+ };
+ this.authProvider = authProvider;
+ this.authCache = authCache;
+ }
+
+ public async Task<bool> RequestAuthentication()
+ {
+ if (AuthToken.AccessToken is null)
+ {
+ await RequestLock.WaitAsync();
+ try
+ {
+ if (AuthToken.AccessToken is null)
+ {
+ Console.WriteLine("[Auth] Request auth tokens");
+
+ if (!authCache.GetToken(AuthToken))
+ {
+ await authProvider.RequestAuthentication(AuthToken);
+ }
+
+ if (AuthToken.AccessToken is null)
+ {
+ Console.WriteLine("[Auth] Authentication Failed");
+ return false;
+ }
+ else
+ {
+ authCache.SaveToken(AuthToken);
+ Console.WriteLine("[Auth] Authentication Success");
+ return true;
+ }
+ }
+ }
+ finally
+ {
+ RequestLock.Release();
+ }
+ }
+ return true;
+ }
+ }
+}
--- /dev/null
+
+namespace MapMyRunService.Models.Auth
+{
+ public class AuthenticationToken
+ {
+ public string ClientId { get; set; } = null;
+ public string ClientSecret { get; set; } = null;
+ public string AccessToken { get; set; } = null;
+ public string RefreshToken { get; set; } = null;
+ }
+}
--- /dev/null
+using System;
+using System.Net.Http;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+using Newtonsoft.Json;
+
+namespace MapMyRunService.Models.Auth
+{
+ /// <summary>
+ /// ExampleAuthProvider uses "Type on watch" authentication method.
+ /// Although this authentication method requires a lot of typing on a small screen,
+ /// It provides the most simple model of user authentication.
+ /// Authentication providers should set up a login page that can be fully displayed on a wearable device screen.
+ /// </summary>
+ public sealed class ExampleAuthProvider : IAuthenticationProvider
+ {
+ private const string AuthServerUri = "http://example.org/authenticate/wearable";
+ private const string AccessTokenKey = "access_token";
+ private const string RefreshTokenKey = "refresh_token";
+
+ private class HttpResponseBody
+ {
+ public Dictionary<string, string> json;
+ }
+
+ public ExampleAuthProvider()
+ {
+ }
+
+ public async Task RequestAuthentication(AuthenticationToken authToken)
+ {
+ try
+ {
+ HttpResponseMessage response = await HttpService.Instance.GetAsync(AuthServerUri);
+
+ if (response.IsSuccessStatusCode)
+ {
+ Console.WriteLine("Example authentication request authentication success");
+ string responseBodyString = await response.Content.ReadAsStringAsync();
+ HttpResponseBody responseBody = JsonConvert.DeserializeObject<HttpResponseBody>(responseBodyString);
+ if (responseBody.json.ContainsKey(AccessTokenKey))
+ {
+ if (responseBody.json.ContainsKey(RefreshTokenKey))
+ {
+ authToken.AccessToken = responseBody.json[AccessTokenKey];
+ authToken.RefreshToken = responseBody.json[RefreshTokenKey];
+ }
+ else
+ {
+ Console.WriteLine($"HttpResponse does not have access token or access token property key does not match {RefreshTokenKey}");
+ }
+ }
+ else
+ {
+ Console.WriteLine($"HttpResponse does not have access token or access token property key does not match {AccessTokenKey}");
+ }
+ }
+ else
+ {
+ Console.WriteLine("Example authentication request authentication failed");
+ }
+ }
+ catch (HttpRequestException e)
+ {
+ // Dummy response for testing with test URL
+ Console.WriteLine($"[Auth] Example authentication - using test authToken");
+ authToken.AccessToken = "test";
+ authToken.RefreshToken = "test";
+ }
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRunService.Models.Auth
+{
+ public interface IAuthCache
+ {
+ bool GetToken(AuthenticationToken authToken);
+
+ void SaveToken(AuthenticationToken authToken);
+
+ void RemoveToken();
+ }
+}
--- /dev/null
+using System.Threading.Tasks;
+
+namespace MapMyRunService.Models.Auth
+{
+ public interface IAuthenticationProvider
+ {
+ Task RequestAuthentication(AuthenticationToken authToken);
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace MapMyRunService.Models
+{
+ public class Dispatcher : IDispatcher
+ {
+ IMessagePortHandler _messagePortHandler = null;
+ private const string UIAppId = "org.tizen.example.MapMyRun";
+ private const string LocalPort = "SERVICE_PORT";
+ private const string RemotePort = "APP_PORT";
+ public const string OperationKey = "operation";
+ public const string DataKey = "data";
+ public const string TransIdKey = "transID";
+ public const string ResponseStatusKey = "status";
+ private const bool SecureMode = false;
+ private uint TransIdIndex = 0;
+ private readonly Dictionary<string, Request> RequestCache = new Dictionary<string, Request>();
+ private readonly List<NotificationProvider> NotificationProviders = new List<NotificationProvider>();
+
+ public static Dispatcher Instance { get; private set; } = new Dispatcher();
+
+ private Dispatcher()
+ {
+ }
+
+ public void Initialize(IMessagePortHandler messagePortHandler)
+ {
+ _messagePortHandler = messagePortHandler;
+ _messagePortHandler.MessageReceived += OnReceiveMessage;
+ _messagePortHandler.Connect(LocalPort, UIAppId, RemotePort, SecureMode);
+ }
+
+ public string SendRequest(Request request)
+ {
+ request.TransID = TransIdIndex.ToString();
+ TransIdIndex++;
+ RequestCache.Add(request.TransID, request);
+ var dataSet = new Dictionary<string, string>
+ {
+ { OperationKey, ((int)request.Operation).ToString() },
+ { DataKey, request.Data },
+ { TransIdKey, request.TransID }
+ };
+
+ _messagePortHandler.Send(dataSet);
+ return request.TransID;
+ }
+
+ public string SendReply(Response response)
+ {
+ var dataSet = new Dictionary<string, string>
+ {
+ { TransIdKey, response.TransID },
+ { DataKey, response.Data },
+ { ResponseStatusKey, response.Status.ToString()}
+ };
+
+ _messagePortHandler.Send(dataSet);
+ return response.TransID;
+ }
+
+ public void OnReceiveMessage(Dictionary<string, string> dataSet)
+ {
+ if (dataSet.TryGetValue(OperationKey, out string operation))
+ {
+ Console.WriteLine($"[Service] Receive Notification: {operation}");
+
+ try
+ {
+ OperationType type = (OperationType)Int32.Parse(operation);
+ //if (dataSet.TryGetValue(DataKey, out string data))
+ // PostNotification(data, type);
+ PostNotification(dataSet, type);
+ }
+ catch
+ {
+ Console.WriteLine("[Service] Invalid Operation");
+ }
+ return;
+ }
+
+ if (!dataSet.TryGetValue(TransIdKey, out string tid))
+ {
+ Console.WriteLine("[Service] Request for operation response is not found");
+ return;
+ }
+
+ if (RequestCache.TryGetValue(tid, out Request cachedRequest))
+ {
+ dataSet.TryGetValue(DataKey, out string data);
+ dataSet.TryGetValue(ResponseStatusKey, out string status);
+ ResponseStatus responseStatus;
+ try
+ {
+ responseStatus = (ResponseStatus)Int32.Parse(status);
+ }
+ catch
+ {
+ responseStatus = ResponseStatus.Fail;
+ }
+
+ var response = new Response(tid, data, responseStatus);
+ cachedRequest.OnReceiveResponse(response);
+ }
+ }
+
+ void PostNotification(Dictionary<string, string> dataSet, OperationType type)
+ {
+ NotificationProvider notificationMonitor;
+ try
+ {
+ notificationMonitor = NotificationProviders.Find(x => x != null && x.Type == type);
+ notificationMonitor.PostNotification(dataSet);
+ }
+ catch
+ {
+ Console.WriteLine($"[Service] Notification Monitor none: {type}");
+ }
+ }
+
+ public bool IsRemoteRunning()
+ {
+ return _messagePortHandler.IsRemoteRunning(UIAppId, RemotePort, SecureMode);
+ }
+
+ public NotificationProvider Listen(OperationType type)
+ {
+ NotificationProvider notificationMonitor;
+ try
+ {
+ notificationMonitor = NotificationProviders.Find(x => x != null && x.Type == type);
+ Console.WriteLine($"[Service] Listen exist Notification Monitor {notificationMonitor.Type}");
+ return notificationMonitor;
+ }
+ catch
+ {
+ notificationMonitor = new NotificationProvider(type);
+ NotificationProviders.Add(notificationMonitor);
+ Console.WriteLine($"[Service] Create new Notification Monitor {notificationMonitor.Type}");
+ return notificationMonitor;
+ }
+ }
+ }
+}
--- /dev/null
+using System.Net.Http;
+
+namespace MapMyRunService.Models
+{
+ public sealed class HttpService : HttpClient
+ {
+ private static HttpService _instance;
+ private static readonly object INSTANCELOCK = new object();
+
+ private HttpService(){
+ }
+
+ public static HttpService Instance
+ {
+ get
+ {
+ if(_instance is null)
+ {
+ lock (INSTANCELOCK)
+ {
+ if(_instance is null)
+ {
+ _instance = new HttpService();
+ }
+ }
+ }
+ return _instance;
+ }
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+using SQLite;
+
+namespace MapMyRunService.Models
+{
+ public interface IDB
+ {
+ SQLiteConnection getConnection();
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace MapMyRunService.Models
+{
+ public interface IDispatcher
+ {
+ void Initialize(IMessagePortHandler messagePortHandler);
+
+ string SendRequest(Request request);
+
+ string SendReply(Response response);
+
+ void OnReceiveMessage(Dictionary<string, string> data);
+
+ bool IsRemoteRunning();
+
+ NotificationProvider Listen(OperationType type);
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace MapMyRunService.Models
+{
+ public interface IMessagePortHandler
+ {
+ void Connect(string localPort, string remoteAppId, string remotePort, bool secureMode);
+
+ bool IsRemoteRunning(string remoteAppId, string remotePort, bool secureMode);
+
+ void Disconnect();
+
+ void Send(Dictionary<string, string> data);
+
+ event Action<Dictionary<string, string>> MessageReceived;
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRunService.Models
+{
+ public interface ITizenServiceApplication
+ {
+ void ExitApplication();
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Timers;
+
+namespace MapMyRunService.Models.KeepAlive
+{
+ class KeepAlive
+ {
+ NotificationObserver _exitAppObserver;
+ readonly IDispatcher _dispatcher;
+ readonly ITizenServiceApplication _tizenServiceApp;
+ private Timer periodicUpdateTimer;
+
+ public KeepAlive(IDispatcher dispatcher, ITizenServiceApplication tizenServiceApp)
+ {
+ _dispatcher = dispatcher;
+ _tizenServiceApp = tizenServiceApp;
+ }
+
+ public void Start()
+ {
+ ListenExitApp();
+
+ periodicUpdateTimer = new Timer(5000);
+ periodicUpdateTimer.Elapsed += new ElapsedEventHandler((s, e) => {
+ Console.WriteLine("[Service] Check keep-alive");
+ if (!_dispatcher.IsRemoteRunning())
+ {
+ Console.WriteLine("UI Applicaions disconnected : ExitApp");
+ _tizenServiceApp.ExitApplication();
+ }
+ });
+ periodicUpdateTimer.AutoReset = true;
+ periodicUpdateTimer.Enabled = true;
+ }
+
+ void ListenExitApp()
+ {
+ _exitAppObserver = new NotificationObserver(OperationType.ExitApp);
+ _exitAppObserver.NotificationReceived += (Dictionary<string, string> data) =>
+ {
+ Console.WriteLine("Receive ExitApp");
+ _tizenServiceApp.ExitApplication();
+ };
+
+ var monitor = _dispatcher.Listen(OperationType.ExitApp);
+ _exitAppObserver.Subscribe(monitor);
+ }
+ }
+}
--- /dev/null
+using MapMyRunService.Models.Auth;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Newtonsoft.Json;
+using System.Threading.Tasks;
+using MapMyRunService.Models.Workout;
+using MapMyRunService.Models.Account;
+
+namespace MapMyRunService.Models
+{
+ public class Loading
+ {
+ readonly string _uiAppId;
+ readonly IDispatcher _dispatcher;
+ readonly IMessagePortHandler _messagePortHandler;
+ private AccountManager accountManager;
+ private IDB _database;
+ private IAuthCache _authCache;
+
+ public Loading(IMessagePortHandler messagePortHandler, string uiAppId, IDB database, IAuthCache authCache)
+ {
+ _messagePortHandler = messagePortHandler;
+ _dispatcher = Dispatcher.Instance;
+ _uiAppId = uiAppId;
+ _database = database;
+ _authCache = authCache;
+ }
+
+ public void StartLoading()
+ {
+ InitializeMessageListeners();
+ WorkoutManager.Instance.InitalizeWorkoutListeners();
+ accountManager = new AccountManager(_database, _dispatcher);
+ accountManager.StartListenGetData();
+ }
+
+ /// <summary>
+ /// Listeners to UI app
+ /// </summary>
+ private void InitializeMessageListeners()
+ {
+ ListenAuth();
+ ListenRequestPermission();
+ }
+
+ private void ListenAuth()
+ {
+ NotificationObserver checkAuthObserver = new NotificationObserver(OperationType.CheckIsAuthenticated);
+ checkAuthObserver.NotificationReceived += async (Dictionary<string, string> data) =>
+ {
+
+ data.TryGetValue("transID", out string transID);
+ data.TryGetValue("data", out string dataValue);
+
+ Console.WriteLine($"[Service] OperationType.CheckIsAuthenticated notif");
+ Console.WriteLine($"transID: {transID}, value: {dataValue}");
+
+ Authentication auth = new Authentication(new ExampleAuthProvider(), _authCache);
+ bool success = await auth.RequestAuthentication();
+ if (success)
+ {
+ // Response payload(second parameter) is empty because the UI app doesn't have to know the access token.
+ // It just needs to know that the request was successful.
+ var response = new Response(transID, "", ResponseStatus.Success);
+ _dispatcher.SendReply(response);
+ }
+ else
+ {
+ Console.WriteLine("[Service] Authentication request was not successful");
+ var response = new Response(transID, "", ResponseStatus.Fail);
+ _dispatcher.SendReply(response);
+ }
+ };
+
+ var monitor = _dispatcher.Listen(OperationType.CheckIsAuthenticated);
+ checkAuthObserver.Subscribe(monitor);
+ }
+
+ private void ListenRequestPermission()
+ {
+ NotificationObserver prepareWkoObserver = new NotificationObserver(OperationType.RequestPermission);
+ prepareWkoObserver.NotificationReceived += (Dictionary<string, string> data) =>
+ {
+
+ data.TryGetValue("transID", out string transID);
+ data.TryGetValue("data", out string dataValue);
+
+ Console.WriteLine($"[Service] OperationType.RequestPermission notif");
+ Console.WriteLine($"transID: {transID}, value: {dataValue}");
+
+ // TOOD: Insert proper response here
+ var response = new Response(transID, "{\"data\": true}", ResponseStatus.Success);
+
+ _dispatcher.SendReply(response);
+ };
+
+ var monitor = _dispatcher.Listen(OperationType.RequestPermission);
+ prepareWkoObserver.Subscribe(monitor);
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRunService.Models
+{
+ public class NotificationObserver : IObserver<Dictionary<string, string>>
+ {
+ public OperationType Type { get; }
+ public event Action<Dictionary<string, string>> NotificationReceived;
+ public event Action<Exception> NotificationErrorReceived;
+ IDisposable _cancellation;
+
+ public NotificationObserver(OperationType type)
+ {
+ Type = type;
+ }
+
+ public void Subscribe(NotificationProvider provider)
+ {
+ _cancellation = provider.Subscribe(this);
+ }
+
+ public void Unsubscribe()
+ {
+ _cancellation.Dispose();
+ }
+
+ public void OnCompleted()
+ {
+ Console.WriteLine($"Notification Observer Completed Type: {Type}");
+ }
+
+ public void OnError(Exception error)
+ {
+ NotificationErrorReceived(error);
+ }
+
+ public void OnNext(Dictionary<string, string> value)
+ {
+ NotificationReceived(value);
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRunService.Models
+{
+ public class NotificationProvider : IObservable<Dictionary<string, string>>
+ {
+ readonly List<IObserver<Dictionary<string, string>>> observers;
+ public OperationType Type { get; }
+ public string Name { get; }
+
+ public NotificationProvider(OperationType type)
+ {
+ Type = type;
+ observers = new List<IObserver<Dictionary<string, string>>>();
+ }
+
+ public void PostNotification(Dictionary<string, string> dataSet)
+ {
+ foreach (var observer in observers)
+ observer.OnNext(dataSet);
+ }
+
+ public IDisposable Subscribe(IObserver<Dictionary<string, string>> observer)
+ {
+ if (!observers.Contains(observer))
+ observers.Add(observer);
+ return new Unsubscriber(observers, observer);
+ }
+
+ private class Unsubscriber : IDisposable
+ {
+ private readonly List<IObserver<Dictionary<string, string>>> _observers;
+ private readonly IObserver<Dictionary<string, string>> _observer;
+
+ public Unsubscriber(List<IObserver<Dictionary<string, string>>> observers, IObserver<Dictionary<string, string>> observer)
+ {
+ _observers = observers;
+ _observer = observer;
+ }
+
+ public void Dispose()
+ {
+ if (_observer != null && _observers.Contains(_observer))
+ _observers.Remove(_observer);
+ }
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace MapMyRunService.Models
+{
+ public enum OperationType
+ {
+ PrepareWKO = 1,
+ StartWKO,
+ PauseWKO,
+ ResumeWKO,
+ FinishWKO,
+ SaveWKO,
+ DiscardWKO,
+ CheckSContext,
+ CheckIsLoggedIn,
+ NavigateToLogin,
+ NavigateToProfile,
+ CheckIsAuthenticated,
+ CheckIsInstalled,
+ GetData,
+ UpdateNutritionPreference,
+ GetSleepScore,
+ ExitApp,
+ MFPGetCurrentSteps,
+ MFPAddEnergy,
+ MFPAddWater,
+ GetSettings,
+ SetSettings,
+ GetWorkoutSettings,
+ SetWorkoutSettings,
+ UnfinishedWorkoutRestore,
+ RestoreWKO,
+ StartGpsAcquire,
+ StopGpsAcquire,
+ CheckNetworkConnection,
+ ChangeNutritionSource,
+ GetConsentStatus,
+ GetUserGear,
+ ConnectUserGear,
+ DisconnectUserGear,
+ RequestPermission,
+ GetTodayTrainingPlans,
+ // More operations to be here
+ OperationMax = GetTodayTrainingPlans,
+ // Notification Type Start
+ WkoDataChanged,
+ HRData,
+ AggregatesData,
+ HRZone,
+ IntensityChanged,
+ GetDiarySummary,
+ GetCachedDiarySummary,
+ StepsChanged,
+ SapInitialized,
+ ApplicationInitialized,
+ ApplicationStarted,
+ NoBluetoothConnection,
+ WristUpEvent,
+ MfpDataUpdated,
+ SapRemoteServiceNotFound,
+ WillpowerChanged,
+ EnergyChanged,
+ SpeedChanged,
+ GpsStateChanged,
+ ShoesStateChanged,
+ MeasurementPreferenceChanged,
+ NutritionPreferencesUpdated,
+ SleepScoreReceived,
+ WorkoutSyncSuccess,
+ WorkoutSyncFailed,
+ UserProfileUpdated,
+ LocationChanged,
+ Logout,
+ SettingsChanged,
+ WorkoutSettingsChanged,
+ SapConnectionTimeout,
+ LangChanged,
+ HapticFeedback,
+ NutritionSourceSupportChanged,
+ CadenceChanged,
+ StrideLengthChanged,
+ RTFCUpdate,
+ RTFCNotification,
+ RTSCUpdate,
+ RTSCNotification,
+ GoalUpdate,
+ GoalCompleted,
+ AppStateForeground,
+ AppStateBackground,
+ // More operations to be here
+ NotificationTypeMax = AppStateBackground
+ }
+
+ public enum DataActionType
+ {
+ None = 0,
+ GetUserProfile,
+ GetUpdatedUserProfile,
+ PostWorkout,
+ GetUserGoals,
+ GetUpToDateUserGoals,
+ GetHrZones,
+ GetChallenges,
+ GetChallengeDetails,
+ GetWorkouts
+ }
+
+ public class Request
+ {
+ public OperationType Operation { get; }
+ public string Data { get; }
+ public event Action<Response> ResponseReceived;
+ public string TransID { get; set; }
+
+ public Request (OperationType operation, string data)
+ {
+ Operation = operation;
+ Data = data;
+ }
+
+ public void OnReceiveResponse(Response response)
+ {
+ ResponseReceived?.Invoke(response);
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace MapMyRunService.Models
+{
+ public enum ResponseStatus
+ {
+ Success = 0,
+ Unsupported,
+ Fail
+ }
+
+ public class Response
+ {
+ public string Data { get; }
+ public string TransID { get; }
+ public ResponseStatus Status { get; }
+
+ public Response(string tid, string data, ResponseStatus status)
+ {
+ TransID = tid;
+ Data = data;
+ Status = status;
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRunService.Models.Settings
+{
+ public class GPSStateEventArgs : EventArgs
+ {
+ public bool enabled;
+ public GPSStateEventArgs()
+ {
+ enabled = false;
+ }
+ }
+
+ public interface IGPSManager
+ {
+ event EventHandler<GPSStateEventArgs> GPSChanged;
+ void EnableGps();
+ void DisableGps();
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRunService.Models.Settings
+{
+ public class SettingsData
+ {
+ public GpsSettingData gps { get; set; }
+ public DebugSettingData debug { get; set; }
+ public ScreenSettingData screenTimeout { get; set; }
+ }
+
+ public class GpsSettingData
+ {
+ public bool systemValue { get; set; }
+ public bool isEnabled { get; set; }
+ public bool isSupported { get; set; }
+ }
+
+ public class DebugSettingData
+ {
+ public FileLogger fileLogger { get; set; }
+ public bool isEnabled { get; set; }
+ public bool isSupported { get; set; }
+ }
+
+ public class ScreenSettingData
+ {
+ public int value { get; set; }
+ public bool isEnabled { get; set; }
+ public bool isSupported { get; set; }
+ }
+
+ public class FileLogger
+ {
+ public bool isEnabled { get; set; }
+ public bool isSupported { get; set; }
+ }
+}
\ No newline at end of file
--- /dev/null
+using System;
+using System.Collections.Generic;
+using Newtonsoft.Json;
+
+namespace MapMyRunService.Models.Settings
+{
+ public class SettingsManager
+ {
+ private IDispatcher _dispatcher;
+ private IGPSManager _gpsmanager;
+ private SettingsData _settings;
+
+ public SettingsManager(IDispatcher dispatcher)
+ {
+ _dispatcher = dispatcher;
+ _settings = new SettingsData();
+ _gpsmanager = null;
+ }
+
+ public void initializeGPS(IGPSManager gpsmanager)
+ {
+ _gpsmanager = gpsmanager;
+ gpsmanager.GPSChanged += (object s, GPSStateEventArgs e) =>
+ {
+ GpsSettingData gps = new GpsSettingData();
+ gps.isEnabled = e.enabled;
+ _settings.gps = gps;
+ };
+ }
+
+ public void Initialize()
+ {
+ ListenStartGpsAcquire();
+ ListenStopGpsAcquire();
+
+ ListenGetSettings();
+ }
+
+ public void ListenGetSettings()
+ {
+ NotificationObserver getSettingsObserver = new NotificationObserver(OperationType.GetSettings);
+
+ getSettingsObserver.NotificationReceived += (Dictionary<string, string> data) =>
+ {
+ data.TryGetValue("transID", out string transID);
+ data.TryGetValue("data", out string dataValue);
+
+ Console.WriteLine($"[Service] OperationType.GetSettings notif");
+ Console.WriteLine($"transID: {transID}, value: {dataValue}");
+
+ string settingsDataJsonString = JsonConvert.SerializeObject(_settings);
+ _dispatcher.SendReply(new Response(transID, settingsDataJsonString, ResponseStatus.Success));
+
+ };
+
+ var monitor = _dispatcher.Listen(OperationType.GetSettings);
+ getSettingsObserver.Subscribe(monitor);
+ }
+
+ public void ListenStartGpsAcquire()
+ {
+ NotificationObserver startGpsAcquire = new NotificationObserver(OperationType.StartGpsAcquire);
+
+ startGpsAcquire.NotificationReceived += (Dictionary<string, string> data) =>
+ {
+ data.TryGetValue("transID", out string transID);
+ data.TryGetValue("data", out string dataValue);
+
+ Console.WriteLine($"[Service] OperationType.StartGpsAcquire notif");
+ Console.WriteLine($"transID: {transID}, value: {dataValue}");
+
+ if(_gpsmanager is null)
+ {
+ Console.WriteLine("GPS Manager is not set");
+ _dispatcher.SendReply(new Response(transID, "", ResponseStatus.Fail));
+ }
+ else
+ {
+ try
+ {
+ _gpsmanager.EnableGps();
+ _dispatcher.SendReply(new Response(transID, "", ResponseStatus.Success));
+ }
+ catch
+ {
+ Console.WriteLine("Error in gps manager");
+ _dispatcher.SendReply(new Response(transID, "", ResponseStatus.Fail));
+ }
+ }
+ };
+
+ var monitor = _dispatcher.Listen(OperationType.StartGpsAcquire);
+ startGpsAcquire.Subscribe(monitor);
+ }
+
+ public void ListenStopGpsAcquire()
+ {
+ NotificationObserver stopGpsAcquire = new NotificationObserver(OperationType.StopGpsAcquire);
+
+ stopGpsAcquire.NotificationReceived += (Dictionary<string, string> data) =>
+ {
+ data.TryGetValue("transID", out string transID);
+ data.TryGetValue("data", out string dataValue);
+
+ Console.WriteLine($"[Service] OperationType.StopGpsAcquire notif");
+ Console.WriteLine($"transID: {transID}, value: {dataValue}");
+
+ if (_gpsmanager is null)
+ {
+ Console.WriteLine("GPS Manager is not set");
+ _dispatcher.SendReply(new Response(transID, "", ResponseStatus.Fail));
+ }
+ else
+ {
+ try
+ {
+ _gpsmanager.DisableGps();
+ _dispatcher.SendReply(new Response(transID, "", ResponseStatus.Success));
+ }
+ catch
+ {
+ Console.WriteLine("Error in gps manager");
+ _dispatcher.SendReply(new Response(transID, "", ResponseStatus.Fail));
+ }
+ }
+ };
+
+ var monitor = _dispatcher.Listen(OperationType.StopGpsAcquire);
+ stopGpsAcquire.Subscribe(monitor);
+ }
+
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace MapMyRunService.Models
+{
+ public class HeartRateData
+ {
+ private int _lastHeartRate = -1;
+ public int LastHeartRate
+ {
+ get
+ {
+ return _lastHeartRate;
+ }
+ set
+ {
+ MinHeartRate = value <= 0 ? 0
+ : value < MinHeartRate ? value : MinHeartRate;
+ MaxHeartRate = value > MaxHeartRate ? value : MaxHeartRate;
+
+ _lastHeartRate = value <= 0 ? 0 : value;
+ AvgHeartRate = value;
+ }
+ }
+
+ private int hrDataPoints { get; set; } = 0;
+ private int _avgHeartRate { get; set; } = 0;
+ public int AvgHeartRate
+ {
+ get
+ {
+
+ return _avgHeartRate / (hrDataPoints < 1 ? 1 : hrDataPoints);
+ }
+ set
+ {
+ if (value <= 0) return;
+
+ _avgHeartRate += value;
+ hrDataPoints++;
+ }
+ }
+
+ public int MinHeartRate { get; set; } = 10000;
+ public int MaxHeartRate { get; set; } = -1;
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace MapMyRunService.Models
+{
+ public class LocationData
+ {
+ public double Latitude { get; set; } = 0;
+ public double Longitude { get; set; } = 0;
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace MapMyRunService.Models
+{
+ public class PedometerData
+ {
+ public uint StepCount { get; set; } = 0;
+ public uint WalkStepCount { get; set; } = 0;
+ public uint RunStepCount { get; set; } = 0;
+ public float CaloriesBurned { get; set; } = 0;
+ public float Distance { get; set; } = 0;
+ public float Speed { get; set; } = 0;
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MapMyRunService.Models.Workout
+{
+ public class WorkoutData
+ {
+ public string calories { get; set; }
+ public string willpower { get; set; }
+ public string hrZone { get; set; }
+ public string intensity { get; set; }
+ public AggregateData aggregates { get; set; }
+ public ActivityData cadence { get; set; }
+ public ActivityData hrData { get; set; }
+ public ActivityData speed { get; set; }
+ public ActivityData strideLength { get; set; }
+ public RangeCoachingResult formCoach { get; set; }
+ public RangeCoachingResult speedCoach { get; set; }
+ public FitnessCoachingResult goalCoach { get; set; }
+ }
+
+ public class ActivityData
+ {
+ public float min { get; set; }
+ public float max { get; set; }
+ public float avg { get; set; }
+ public float latest { get; set; }
+ public string type { get; set; }
+ }
+
+ public class AggregateData
+ {
+ public float distance_total { get; set; }
+ public float metabolic_energy_total { get; set; }
+ public float intensity_avg { get; set; }
+ public float willpower { get; set; }
+ public int active_time_total { get; set; }
+ public int elapsed_time_total { get; set; }
+ public int steps_total { get; set; }
+ public int heartrate_avg { get; set; }
+ public int heartrate_max { get; set; }
+ public float speed_max { get; set; }
+ public float speed_avg { get; set; }
+ public float cadence_avg { get; set; }
+ public float cadence_max { get; set; }
+ public float stride_length_avg { get; set; }
+ }
+
+ public class RangeCoachingResult
+ {
+ public float targetMax { get; set; }
+ public float targetMin { get; set; }
+ public float currentValue { get; set; }
+ public int status { get; set; }
+ public bool notif { get; set; }
+ }
+
+ public class FitnessCoachingResult
+ {
+ public float current { get; set; }
+ public float target { get; set; }
+ public float percentage { get; set; }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Timers;
+using Newtonsoft.Json;
+
+
+namespace MapMyRunService.Models.Workout
+{
+ public class WorkoutManager
+ {
+ private IDispatcher _dispatcher;
+
+ public event Action<WorkoutReport> PrepareWKO = delegate { };
+ public event Action<WorkoutReport> StartWKO = delegate { };
+ public event Action<WorkoutReport> PauseWKO = delegate { };
+ public event Action<WorkoutReport> FinishWKO = delegate { };
+ public event Action<WorkoutReport> ResumeWKO = delegate { };
+ public event Action<WorkoutReport> ReadData = delegate { };
+
+ private Timer periodicUpdateTimer;
+ private bool shouldSendUpdates;
+ private double updateInProgress; // 0 - false, 1 - true
+ private WorkoutReport currentWorkoutReport;
+
+ public static WorkoutManager Instance { get; private set; } = new WorkoutManager();
+ private WorkoutManager()
+ {
+ _dispatcher = Dispatcher.Instance;
+ InitializePeriodicReporter();
+ }
+
+ private void InitializePeriodicReporter()
+ {
+ updateInProgress = 0;
+ shouldSendUpdates = false;
+ periodicUpdateTimer = new Timer(1000);
+ periodicUpdateTimer.Elapsed += new ElapsedEventHandler((s, e) => { UpdateWKOData(); });
+ periodicUpdateTimer.AutoReset = true;
+ }
+
+ private void disablePeriodicReporter()
+ {
+ shouldSendUpdates = false;
+ periodicUpdateTimer.Enabled = false;
+ }
+
+ private void enablePeriodicReporter()
+ {
+ shouldSendUpdates = true;
+ periodicUpdateTimer.Enabled = true;
+ }
+
+ public void InitalizeWorkoutListeners()
+ {
+ ListenPrepareWKO();
+ ListenStartWKO();
+ ListenPauseWKO();
+ ListenFinishWKO();
+ ListenResumeWKO();
+ }
+
+ private void ListenPrepareWKO()
+ {
+ currentWorkoutReport = new WorkoutReport();
+
+ NotificationObserver prepareWkoObserver = new NotificationObserver(OperationType.PrepareWKO);
+ prepareWkoObserver.NotificationReceived += (Dictionary<string, string> data) =>
+ {
+
+ data.TryGetValue("transID", out string transID);
+ data.TryGetValue("data", out string dataValue);
+
+ Console.WriteLine($"[Service] OperationType.PrepareWKO notif");
+ Console.WriteLine($"transID: {transID}, value: {dataValue}");
+
+ // TOOD: Insert proper response here
+ var response = new Response(transID, "{\"data\": 42}", ResponseStatus.Success);
+
+ _dispatcher.SendReply(response);
+
+ PrepareWKO?.Invoke(currentWorkoutReport);
+ };
+
+ var monitor = _dispatcher.Listen(OperationType.PrepareWKO);
+ prepareWkoObserver.Subscribe(monitor);
+ }
+
+ private void ListenStartWKO()
+ {
+ NotificationObserver prepareWkoObserver = new NotificationObserver(OperationType.StartWKO);
+ prepareWkoObserver.NotificationReceived += (Dictionary<string, string> data) =>
+ {
+
+ data.TryGetValue("transID", out string transID);
+ data.TryGetValue("data", out string dataValue);
+
+ Console.WriteLine($"[Service] OperationType.StartWKO notif");
+ Console.WriteLine($"transID: {transID}, value: {dataValue}");
+
+ // TOOD: Insert proper response here
+ var response = new Response(transID, "{\"data\": 42}", ResponseStatus.Success);
+
+ _dispatcher.SendReply(response);
+
+ StartWKO?.Invoke(currentWorkoutReport);
+
+ enablePeriodicReporter();
+ };
+
+ var monitor = _dispatcher.Listen(OperationType.StartWKO);
+ prepareWkoObserver.Subscribe(monitor);
+ }
+
+ private void ListenFinishWKO()
+ {
+ NotificationObserver prepareWkoObserver = new NotificationObserver(OperationType.FinishWKO);
+ prepareWkoObserver.NotificationReceived += (Dictionary<string, string> data) =>
+ {
+
+ data.TryGetValue("transID", out string transID);
+ data.TryGetValue("data", out string dataValue);
+
+ Console.WriteLine($"[Service] OperationType.FinishWKO notif");
+ Console.WriteLine($"transID: {transID}, value: {dataValue}");
+
+ // TOOD: Insert proper response here
+ var response = new Response(transID, "{\"data\": 42}", ResponseStatus.Success);
+
+ disablePeriodicReporter();
+ _dispatcher.SendReply(response);
+ FinishWKO?.Invoke(currentWorkoutReport);
+ };
+
+ var monitor = _dispatcher.Listen(OperationType.FinishWKO);
+ prepareWkoObserver.Subscribe(monitor);
+ }
+
+ private void ListenResumeWKO()
+ {
+ NotificationObserver prepareWkoObserver = new NotificationObserver(OperationType.ResumeWKO);
+ prepareWkoObserver.NotificationReceived += (Dictionary<string, string> data) =>
+ {
+
+ data.TryGetValue("transID", out string transID);
+ data.TryGetValue("data", out string dataValue);
+
+ Console.WriteLine($"[Service] OperationType.ResumeWKO notif");
+ Console.WriteLine($"transID: {transID}, value: {dataValue}");
+
+ // TOOD: Insert proper response here
+ var response = new Response(transID, "{\"data\": 42}", ResponseStatus.Success);
+
+ enablePeriodicReporter();
+ _dispatcher.SendReply(response);
+ ResumeWKO?.Invoke(currentWorkoutReport);
+ };
+
+ var monitor = _dispatcher.Listen(OperationType.ResumeWKO);
+ prepareWkoObserver.Subscribe(monitor);
+ }
+
+ private void ListenPauseWKO()
+ {
+ NotificationObserver prepareWkoObserver = new NotificationObserver(OperationType.PauseWKO);
+ prepareWkoObserver.NotificationReceived += (Dictionary<string, string> data) =>
+ {
+
+ data.TryGetValue("transID", out string transID);
+ data.TryGetValue("data", out string dataValue);
+
+ Console.WriteLine($"[Service] OperationType..PauseWKO notif");
+ Console.WriteLine($"transID: {transID}, value: {dataValue}");
+
+ // TOOD: Insert proper response here
+ var response = new Response(transID, "{\"data\": 42}", ResponseStatus.Success);
+
+ disablePeriodicReporter();
+ _dispatcher.SendReply(response);
+ PauseWKO?.Invoke(currentWorkoutReport);
+ };
+
+ var monitor = _dispatcher.Listen(OperationType.PauseWKO);
+ prepareWkoObserver.Subscribe(monitor);
+ }
+
+ private void UpdateWKOData()
+ {
+ if (shouldSendUpdates &&
+ System.Threading.Interlocked.CompareExchange(ref updateInProgress, 1, 0) == 0)
+ {
+ Console.WriteLine("START Update WKO Data >>>>>");
+ ReadData?.Invoke(currentWorkoutReport);
+
+ string jsonReport = JsonConvert.SerializeObject(currentWorkoutReport.workoutData);
+
+ var request = new Request(OperationType.WkoDataChanged, jsonReport);
+ request.ResponseReceived += (Response response) =>
+ {
+ Console.WriteLine($"[Service] UpdateWKOData acknowledged.");
+ };
+ _dispatcher.SendRequest(request);
+
+ updateInProgress = 0;
+ }
+ }
+ }
+
+}
--- /dev/null
+using MapMyRunService.Models.Workout;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace MapMyRunService.Models
+{
+ public class WorkoutReport
+ {
+ public DateTime StartTime { get; set; }
+ public int TimePaused { get; set; }
+ public DateTime EndTime { get; set; }
+ public LocationData LocStart { get; set; }
+ public LocationData LocEnd { get; set; }
+
+ public WorkoutData workoutData { get; set; }
+
+ public WorkoutReport()
+ {
+ LocStart = new LocationData();
+ LocEnd = new LocationData();
+ }
+ }
+}