diff --git a/Covid19Radar/Covid19Radar.Android/Covid19Radar.Android.csproj b/Covid19Radar/Covid19Radar.Android/Covid19Radar.Android.csproj index 567133036..b509ba183 100644 --- a/Covid19Radar/Covid19Radar.Android/Covid19Radar.Android.csproj +++ b/Covid19Radar/Covid19Radar.Android/Covid19Radar.Android.csproj @@ -39,7 +39,7 @@ SdkOnly false true - false + true CJK armeabi-v7a;x86;x86_64;arm64-v8a true @@ -112,12 +112,13 @@ prompt MinimumRecommendedRules.ruleset true - false true + true apk true CJK Xamarin.Android.Net.AndroidClientHandler + None diff --git a/Covid19Radar/Covid19Radar/App.xaml.cs b/Covid19Radar/Covid19Radar/App.xaml.cs index 7100d5450..95c5f98fe 100644 --- a/Covid19Radar/Covid19Radar/App.xaml.cs +++ b/Covid19Radar/Covid19Radar/App.xaml.cs @@ -128,6 +128,9 @@ protected override void RegisterTypes(IContainerRegistry containerRegistry) containerRegistry.RegisterForNavigation(); containerRegistry.RegisterForNavigation(); containerRegistry.RegisterForNavigation(); +#if DEBUG + containerRegistry.RegisterForNavigation(); +#endif // Settings containerRegistry.RegisterForNavigation(); diff --git a/Covid19Radar/Covid19Radar/Covid19Radar.csproj b/Covid19Radar/Covid19Radar/Covid19Radar.csproj index 9a434c0c5..0ac7e11a1 100644 --- a/Covid19Radar/Covid19Radar/Covid19Radar.csproj +++ b/Covid19Radar/Covid19Radar/Covid19Radar.csproj @@ -153,4 +153,9 @@ + + + + + diff --git a/Covid19Radar/Covid19Radar/Services/HttpDataServiceMock.cs b/Covid19Radar/Covid19Radar/Services/HttpDataServiceMock.cs index fe7a9dd1f..31ce67b5f 100644 --- a/Covid19Radar/Covid19Radar/Services/HttpDataServiceMock.cs +++ b/Covid19Radar/Covid19Radar/Services/HttpDataServiceMock.cs @@ -3,17 +3,43 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ using Covid19Radar.Model; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Net; using System.Threading; using System.Threading.Tasks; +using System.Linq; +using System.Net.Http; +using Covid19Radar.Common; +using Newtonsoft.Json; namespace Covid19Radar.Services { class HttpDataServiceMock : IHttpDataService { + private readonly HttpClient downloadClient; + private readonly MockCommonUtils mockCommonUtils = new MockCommonUtils(); + + public HttpDataServiceMock(IHttpClientService httpClientService) + { + downloadClient = httpClientService.Create(); + } + + // copy from ./HttpDataService.cs + private async Task GetCdnAsync(string url, CancellationToken cancellationToken) + { + HttpResponseMessage result = await downloadClient.GetAsync(url, cancellationToken); + await result.Content.ReadAsStringAsync(); + + if (result.StatusCode == System.Net.HttpStatusCode.OK) + { + return await result.Content.ReadAsStringAsync(); + } + return null; + } + public Task MigrateFromUserData(UserDataModel userData) { return Task.CompletedTask; @@ -37,27 +63,119 @@ Task IHttpDataService.GetTemporaryExposureKey(string url, CancellationTo }); } - Task> IHttpDataService.GetTemporaryExposureKeyList(string region, CancellationToken cancellationToken) + private TemporaryExposureKeyExportFileModel CreateTestData(long created) { - return Task.Factory.StartNew>(() => + return new TemporaryExposureKeyExportFileModel() { - Debug.WriteLine("HttpDataServiceMock::GetTemporaryExposureKeyList called"); - return new List(); - }); + Region = "440", + Url = "testUrl", + Created = created + }; + } + + private long CalcTimeAddDays(int day) + => new DateTimeOffset(DateTime.UtcNow.AddDays(day)).ToUnixTimeMilliseconds(); + + private long CalcMidnightTimeAddDays(int day) + { + DateTime d = DateTime.UtcNow.AddDays(day); + // set 0 hour,1 min,2 sec,3 millisecond for debug + return new DateTimeOffset(new DateTime(d.Year, d.Month, d.Day, 0, 1, 2, 3)).ToUnixTimeMilliseconds(); } + enum PresetTekListType // PresetTekListData for Tek List + { + Nothing = 0, //nothing (default for v1.2.3) + RealTime = 1, // real time + MidnightTime = 2, // last night + // please add "YourDataType = " + } + + private List PresetTekListData(int dataVersion) + { + switch ((PresetTekListType)dataVersion) + { + case PresetTekListType.MidnightTime: + return new List { CreateTestData(CalcMidnightTimeAddDays(-1)), CreateTestData(CalcMidnightTimeAddDays(0)) }; + case PresetTekListType.RealTime: + return new List { CreateTestData(CalcTimeAddDays(-1)), CreateTestData(CalcTimeAddDays(0)) }; + case PresetTekListType.Nothing: + default: + return new List(); + } + } + + async Task> IHttpDataService.GetTemporaryExposureKeyList(string region, CancellationToken cancellationToken) + { + /* CdnUrlBase trick for Debug_Mock + "https://www.example.com/"(url with 2+ periods) -> download "url"+"c19r/440/list.json". IsDownloadRequired + "1598022036649,1598022036751,1598022036826" -> direct input timestamps. IsDirectInput + "https://CDN_URL_BASE/2" -> dataVersion = 2 + "https://CDN_URL_BASE/" -> dataVersion = 0 (default) + */ + //string url = AppSettings.Instance.CdnUrlBase; + if (mockCommonUtils.IsDownloadRequired()) + { + // copy from GetTemporaryExposureKeyList @ ./HttpDataService.cs and delete logger part + var container = AppSettings.Instance.BlobStorageContainerName; + var urlJson = AppSettings.Instance.CdnUrlBase + $"{container}/{region}/list.json"; + var result = await GetCdnAsync(urlJson, cancellationToken); + if (result != null) + { + Debug.WriteLine("HttpDataServiceMock::GetTemporaryExposureKeyList downloaded"); + return JsonConvert.DeserializeObject>(result); + } + else + { + Debug.WriteLine("HttpDataServiceMock::GetTemporaryExposureKeyList download failed"); + return new List(); + } + } + else if (mockCommonUtils.IsDirectInput()) + { + Debug.WriteLine("HttpDataServiceMock::GetTemporaryExposureKeyList direct data called"); + return (mockCommonUtils.GetCreatedTimes().Select(x => CreateTestData(Convert.ToInt64(x))).ToList()); + } + else + { + Debug.WriteLine("HttpDataServiceMock::GetTemporaryExposureKeyList preset data called"); + return PresetTekListData(mockCommonUtils.GetTekListDataType()); + } + } + + async Task IHttpDataService.PostRegisterUserAsync() { Debug.WriteLine("HttpDataServiceMock::PostRegisterUserAsync called"); - return await Task.FromResult(true); + var result = mockCommonUtils.GetRegisterDataType() switch + { + 1 => false, + _ => true + }; + return await Task.FromResult(result); } Task IHttpDataService.PutSelfExposureKeysAsync(DiagnosisSubmissionParameter request) { + var code = HttpStatusCode.OK; // default. for PutSelfExposureKeys NG + var dataType = mockCommonUtils.GetDiagnosisDataType(); + if (dataType >= 100) // HttpStatusCode >=100 by RFC2616#section-10 + { + code = (HttpStatusCode)dataType; + } + else + { + switch (dataType) + { + case 1: + code = HttpStatusCode.NoContent; // for Successful PutSelfExposureKeys + break; + } + } return Task.Factory.StartNew(() => { Debug.WriteLine("HttpDataServiceMock::PutSelfExposureKeysAsync called"); - return HttpStatusCode.OK; + return code; }); } diff --git a/Covid19Radar/Covid19Radar/Services/TestNativeImplementation.cs b/Covid19Radar/Covid19Radar/Services/TestNativeImplementation.cs index f32720ee3..c44356fec 100644 --- a/Covid19Radar/Covid19Radar/Services/TestNativeImplementation.cs +++ b/Covid19Radar/Covid19Radar/Services/TestNativeImplementation.cs @@ -4,79 +4,200 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Xamarin.Essentials; using Xamarin.ExposureNotifications; namespace Covid19Radar.Services { - public class TestNativeImplementation : INativeImplementation - { - static readonly Random random = new Random(); - - Task WaitRandom() - => Task.Delay(random.Next(100, 2500)); - - public async Task StartAsync() - { - await WaitRandom(); - Preferences.Set("fake_enabled", true); - } - - public async Task StopAsync() - { - await WaitRandom(); - Preferences.Set("fake_enabled", true); - } - - public async Task IsEnabledAsync() - { - await WaitRandom(); - return Preferences.Get("fake_enabled", true); - } - - public async Task> GetSelfTemporaryExposureKeysAsync() - { - var keys = new List(); - - for (var i = 1; i < 14; i++) - keys.Add(GenerateRandomKey(i)); - - await WaitRandom(); - - return keys; - } - - public Task GetStatusAsync() - => Task.FromResult(Preferences.Get("fake_enabled", true) ? Status.Active : Status.Disabled); - - public Task<(ExposureDetectionSummary summary, Func>> getInfo)> DetectExposuresAsync(IEnumerable files) - { - var summary = new ExposureDetectionSummary(10, 2, 5); - - Task> GetInfo() - { - var info = new List - { - new ExposureInfo (DateTime.UtcNow.AddDays(-10), TimeSpan.FromMinutes(15), 65, 5, RiskLevel.Medium), - new ExposureInfo (DateTime.UtcNow.AddDays(-11), TimeSpan.FromMinutes(5), 40, 3, RiskLevel.Low), - }; - return Task.FromResult>(info); - } - - return Task.FromResult<(ExposureDetectionSummary, Func>>)>((summary, GetInfo)); - } - - static TemporaryExposureKey GenerateRandomKey(int daysAgo) - { - var buffer = new byte[16]; - random.NextBytes(buffer); - - return new TemporaryExposureKey( - buffer, - DateTimeOffset.UtcNow.AddDays(-1 * daysAgo), - TimeSpan.FromMinutes(random.Next(5, 120)), - (RiskLevel)random.Next(1, 8)); - } - } + public class MockCommonUtils + { + public string CdnUrlBase { get => AppSettings.Instance.CdnUrlBase; } + public string ApiUrlBase { get => AppSettings.Instance.ApiUrlBase; } + + public bool IsDownloadRequired() + => Regex.IsMatch(CdnUrlBase, @"^https://.*\..*\..*/$"); + + public bool IsDirectInput() + => Regex.IsMatch(CdnUrlBase, @"^(\d+,)+\d+,*$"); + + + private ushort NumberEndofSentence(string url) + { + Match match = Regex.Match(url, @"(?\d+)$"); + ushort number = 0; + if (match.Success) + { + number = Convert.ToUInt16(match.Groups["d"].Value); + } + return number; + } + public List GetCreatedTimes() + => CdnUrlBase.Split(",").ToList(); + public ushort GetTekListDataType() + => NumberEndofSentence(CdnUrlBase); + public string[] GetApiUrlSegment() + { + // "url/api" -> { "url/api", "", "" } + // "url/base/api/register1/diagnosis2" -> { "url/base/api", "/register1", "/diagnosis2" } + // "url/api1/r1/d2" -> { "url/api1", "/r1", "/d2" } + // "url/api1/d2/r1" -> { "url/api1", "/r1", "/d2" } + var url = ApiUrlBase; + var r = new Regex("/r(egister)?[0-9]+"); + var d = new Regex("/d(iagnosis)?[0-9]+"); + var urlRegister = r.Match(url).Value; + url = r.Replace(url, ""); + var urlDiagnosis = d.Match(url).Value; + url = d.Replace(url, ""); + var urlApi = url; + return new string[] { urlApi, urlRegister, urlDiagnosis }; + } + public ushort GetDiagnosisDataType() + => NumberEndofSentence(GetApiUrlSegment()[2]); + public ushort GetRegisterDataType() + => NumberEndofSentence(GetApiUrlSegment()[1]); + public ushort GetApiDataType() + => NumberEndofSentence(GetApiUrlSegment()[0]); + public bool IsDirectInputApi() + => Regex.IsMatch(GetApiUrlSegment()[0], @"^(\d+,)+\d+,?$"); + public List GetApiStrings() + => GetApiUrlSegment()[0].Split(",").ToList(); + } + public class TestNativeImplementation : INativeImplementation + { + static readonly Random random = new Random(); + private readonly MockCommonUtils mockCommonUtils = new MockCommonUtils(); + const int DAY_OF_TEK_STORED = 14; // used only in GetSelfTemporaryExposureKeysAsync + const int KEY_DATA_LENGTH = 16; // used only in GenerateRandomKey + + Task WaitRandom() + => Task.Delay(random.Next(100, 2500)); + + public async Task StartAsync() + { + await WaitRandom(); + Preferences.Set("fake_enabled", true); + } + + public async Task StopAsync() + { + await WaitRandom(); + Preferences.Set("fake_enabled", false); + } + + public async Task IsEnabledAsync() + { + await WaitRandom(); + return Preferences.Get("fake_enabled", true); + } + + public async Task> GetSelfTemporaryExposureKeysAsync() + { + var keys = new List(); + for (var i = 1; i <= DAY_OF_TEK_STORED; i++) + { + keys.Add(GenerateRandomKey(i)); + } + await WaitRandom(); + + return keys; + } + + public Task GetStatusAsync() + => Task.FromResult(Preferences.Get("fake_enabled", true) ? Status.Active : Status.Disabled); + + enum PresetDataType + { + TwoLowRiskMatches = 0, // two low-risk matches (default for v1.2.3) + OneHighRiskMatchAnd2LowRiskMatches = 1, // one high-risk match and 2 low-risk matches + NoMatch = 2, + // please add "YourDataType = " + } + + private ushort[] DataPreset(int dataType) + { + /* DataPreset returns ushort[]; + index[0] ~ index[4] : data for ExposureDetectionSummary + index[5] ~ index[10] : 1st data for ExposureInfo + index[11] ~ index[15] : 2nd data for ExposureInfo + index[16] ~ index[20] : 3rd data for ExposureInfo + .... + */ + switch ((PresetDataType)dataType) + { + case PresetDataType.OneHighRiskMatchAnd2LowRiskMatches: + return ( + new ushort[] {10, 3, 27, 0, 0, // ExposureDetectionSummary + 13, 15, 65, 27, (ushort)RiskLevel.High, // ExposureInfo 1st + 10, 15, 65, 5, (ushort)RiskLevel.Medium, // ExposureInfo 2st + 11, 5, 40, 3, (ushort)RiskLevel.Low, // ExposureInfo 3nd + }); + case PresetDataType.NoMatch: + return ( + new ushort[] {0, 0, 0, 0, 0, // ExposureDetectionSummary + }); + case PresetDataType.TwoLowRiskMatches: + default: + return ( + new ushort[] {10, 2, 5, 0, 0, // ExposureDetectionSummary + 10, 15, 65, 5, (ushort)RiskLevel.Medium, // ExposureInfo 1st (RiskLevel.Medium=4) + 11, 5, 40, 3, (ushort)RiskLevel.Low, // ExposureInfo 2nd(RiskLevel.Low=2) + }); + } + } + + private ushort[] CreatePresetData() + { + if (mockCommonUtils.IsDirectInputApi()) + { + return mockCommonUtils.GetApiStrings().Select(x => Convert.ToUInt16(x)).ToArray(); + } + return DataPreset(mockCommonUtils.GetApiDataType()); + + } + + public Task<(ExposureDetectionSummary summary, Func>> getInfo)> DetectExposuresAsync(IEnumerable files) + { + /* ApiUrlBase trick for Debug_Mock + "10,2,5,0,0,10,15,65,5,4,11,5,40,3,2" -> direct input (the same with default) + "https://API_URL_BASE/api2" -> dataVer = 2 + "https://API_URL_BASE/api" -> dataVer = 0 (default) + others -> dataVer is the number at the end of the sentence + */ + var dataPreset = CreatePresetData(); + int index = 0; + var summary = new ExposureDetectionSummary(dataPreset[index++], dataPreset[index++], + dataPreset[index++], new TimeSpan[dataPreset[index++]], dataPreset[index++]); + // c.f.: ExposureDetectionSummary(daysSinceLastExposure=dataPreset[0],matchedKeyCount=dataPreset[1],maximumRiskScore=dataPreset[2],attenuationDurations=new TimeSpan[dataPreset[3]],summationRiskScore=dataPreset[4]) + + Task> GetInfo() + { + var info = new List(); + while (index < dataPreset.Length) + { + info.Add(new ExposureInfo(DateTime.UtcNow.AddDays(-dataPreset[index++]), + TimeSpan.FromMinutes(dataPreset[index++]), dataPreset[index++], + dataPreset[index++], (Xamarin.ExposureNotifications.RiskLevel)dataPreset[index++])); + // c.f.: ExposureInfo(DateTime timestamp, TimeSpan duration, int attenuationValue, int totalRiskScore, RiskLevel riskLevel) + }; + return Task.FromResult>(info); + } + + return Task.FromResult<(ExposureDetectionSummary, Func>>)>((summary, GetInfo)); + } + + static TemporaryExposureKey GenerateRandomKey(int daysAgo) + { + var keyData = new byte[KEY_DATA_LENGTH]; + random.NextBytes(keyData); + + return new TemporaryExposureKey( + keyData, + DateTimeOffset.UtcNow.AddDays(-1 * daysAgo), + TimeSpan.FromMinutes(random.Next(5, 120)), + (RiskLevel)random.Next(1, 8)); + } + } } diff --git a/Covid19Radar/Covid19Radar/ViewModels/MenuPageViewModel.cs b/Covid19Radar/Covid19Radar/ViewModels/MenuPageViewModel.cs index cc187c1c5..714d26011 100644 --- a/Covid19Radar/Covid19Radar/ViewModels/MenuPageViewModel.cs +++ b/Covid19Radar/Covid19Radar/ViewModels/MenuPageViewModel.cs @@ -78,6 +78,16 @@ public MenuPageViewModel(INavigationService navigationService) : base(navigation IconColor = "#019AE8", TextColor = "#000" }); +#if DEBUG + MenuItems.Add(new MainMenuModel() + { + Icon = "\uf013", + PageName = nameof(DebugPage), + Title = "Debug", + IconColor = "#019AE8", + TextColor = "#000" + }); +#endif NavigateCommand = new DelegateCommand(Navigate); } diff --git a/Covid19Radar/Covid19Radar/ViewModels/Settings/DebugPageViewModel.cs b/Covid19Radar/Covid19Radar/ViewModels/Settings/DebugPageViewModel.cs new file mode 100644 index 000000000..9d6de48f2 --- /dev/null +++ b/Covid19Radar/Covid19Radar/ViewModels/Settings/DebugPageViewModel.cs @@ -0,0 +1,192 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +using System; +using System.Linq; +using Acr.UserDialogs; +using Covid19Radar.Services; +using Prism.Navigation; +using Xamarin.Forms; + +namespace Covid19Radar.ViewModels +{ + public class DebugPageViewModel : ViewModelBase + { + private readonly IUserDataService _userDataService; + private readonly ITermsUpdateService _termsUpdateService_; + private readonly IExposureNotificationService _exposureNotificationService; + + private string _debugInfo; + public string DebugInfo + { + get { return _debugInfo; } + set { SetProperty(ref _debugInfo, value); } + } + + public async void UpdateInfo(string exception = "") + { + string os = Device.RuntimePlatform; +#if DEBUG + os += ",DEBUG"; +#endif +#if USE_MOCK + os += ",USE_MOCK"; +#endif + + // debug info for ./SplashPageViewModel.cs + var termsUpdateInfo = await _termsUpdateService_.GetTermsUpdateInfo() ?? new Model.TermsUpdateInfoModel(); + + var termsOfServiceUpdateDateTime = "Not Available"; + if (termsUpdateInfo.TermsOfService != null) + { + termsOfServiceUpdateDateTime = termsUpdateInfo.TermsOfService.UpdateDateTime.ToString(); + } + + var privacyPolicyUpdateDateTime = "Not Available"; + if (termsUpdateInfo.PrivacyPolicy != null) + { + privacyPolicyUpdateDateTime = termsUpdateInfo.PrivacyPolicy.UpdateDateTime.ToString(); + } + + var lastProcessTekTimestampList = AppSettings.Instance.SupportedRegions.Select(region => + new LastProcessTekTimestamp() + { + Region = region, + Ticks = _exposureNotificationService.GetLastProcessTekTimestamp(region) + }.ToString() + ); + + string regionString = string.Join(",", AppSettings.Instance.SupportedRegions); + string lastProcessTekTimestampsStr = string.Join("\n ", lastProcessTekTimestampList); + + var exposureNotificationStatus = await Xamarin.ExposureNotifications.ExposureNotification.IsEnabledAsync(); + var exposureNotificationMessage = await _exposureNotificationService.UpdateStatusMessageAsync(); + + // ../../settings.json + var str = new[] { + $"Build: {os}", + $"Version: {AppSettings.Instance.AppVersion}", + $"Region: {regionString}", + $"CdnUrl: {AppSettings.Instance.CdnUrlBase}", + $"ApiUrl: {AppSettings.Instance.ApiUrlBase}", + $"TermsOfServiceUpdatedDateTime: {termsOfServiceUpdateDateTime}", + $"PrivacyPolicyUpdatedDateTime: {privacyPolicyUpdateDateTime}", + $"StartDate: {_userDataService.GetStartDate().ToLocalTime().ToString("F")}", + $"DaysOfUse: {_userDataService.GetDaysOfUse()}", + $"ExposureCount: {_exposureNotificationService.GetExposureCountToDisplay()}", + $"LastProcessTekTimestamp: {lastProcessTekTimestampsStr}", + $"ENstatus: {exposureNotificationStatus}", + $"ENmessage: {exposureNotificationMessage}", + $"Now: {DateTime.Now.ToLocalTime().ToString("F")}", + exception + }; + DebugInfo = string.Join(Environment.NewLine, str); + } + + public DebugPageViewModel( + INavigationService navigationService, + IUserDataService userDataService, + ITermsUpdateService termsUpdateService, + IExposureNotificationService exposureNotificationService + ) : base(navigationService) + { + Title = "Title:Debug"; + _userDataService = userDataService; + _termsUpdateService_ = termsUpdateService; + _exposureNotificationService = exposureNotificationService; + } + + public override void Initialize(INavigationParameters parameters) + { + base.Initialize(parameters); + UpdateInfo("Initialize"); + } + + public Command OnClickReload => new Command(() => UpdateInfo("Reload")); + + public Command OnClickStartExposureNotification => new Command(async () => + { + UserDialogs.Instance.ShowLoading("Starting ExposureNotification..."); + var result = await _exposureNotificationService.StartExposureNotification(); + var message = $"Result: {result}"; + UserDialogs.Instance.HideLoading(); + await UserDialogs.Instance.AlertAsync(message, "StartExposureNotification", Resources.AppResources.ButtonOk); + UpdateInfo("StartExposureNotification"); + }); + + public Command OnClickFetchExposureKeyAsync => new Command(async () => + { + var exception = "FetchExposureKeyAsync"; + try + { + await _exposureNotificationService.FetchExposureKeyAsync(); + } + catch (Exception ex) + { + exception += $":Exception: {ex}"; + } + UpdateInfo(exception); + }); + + // see ../Settings/SettingsPageViewModel.cs + public Command OnClickStopExposureNotification => new Command(async () => + { + UserDialogs.Instance.ShowLoading("Stopping ExposureNotification..."); + var result = await _exposureNotificationService.StopExposureNotification(); + string message = $"Result: {result}"; + UserDialogs.Instance.HideLoading(); + await UserDialogs.Instance.AlertAsync(message, "StopExposureNotification", Resources.AppResources.ButtonOk); + UpdateInfo("StopExposureNotification"); + }); + + public Command OnClickRemoveStartDate => new Command(() => + { + _userDataService.RemoveStartDate(); + UpdateInfo("RemoveStartDate"); + }); + + public Command OnClickRemoveExposureInformation => new Command(() => + { + _exposureNotificationService.RemoveExposureInformation(); + UpdateInfo("RemoveExposureInformation"); + }); + + public Command OnClickRemoveConfiguration => new Command(() => + { + _exposureNotificationService.RemoveConfiguration(); + UpdateInfo("RemoveConfiguration"); + }); + + public Command OnClickRemoveLastProcessTekTimestamp => new Command(() => + { + _exposureNotificationService.RemoveLastProcessTekTimestamp(); + UpdateInfo("RemoveLastProcessTekTimestamp"); + }); + + public Command OnClickRemoveAllUpdateDate => new Command(() => + { + _termsUpdateService_.RemoveAllUpdateDate(); + UpdateInfo("RemoveAllUpdateDate"); + }); + + public Command OnClickQuit => new Command(() => + { + Application.Current.Quit(); + DependencyService.Get().closeApplication(); + }); + + private class LastProcessTekTimestamp + { + internal string Region; + + internal long Ticks; + + internal DateTimeOffset DateTime + => DateTimeOffset.FromUnixTimeMilliseconds(Ticks).ToLocalTime(); + + public override string ToString() + => $"{Region} - {DateTime:F}({Ticks})"; + } + } +} diff --git a/Covid19Radar/Covid19Radar/Views/Settings/DebugPage.xaml b/Covid19Radar/Covid19Radar/Views/Settings/DebugPage.xaml new file mode 100644 index 000000000..b69e5090b --- /dev/null +++ b/Covid19Radar/Covid19Radar/Views/Settings/DebugPage.xaml @@ -0,0 +1,212 @@ + + + + + + + + + +