public VerifiedParticipationPage(Protocol protocol, ParticipationRewardDatum participationRewardDatum) { Title = "Participation Verification"; StackLayout contentLayout = new StackLayout { Orientation = StackOrientation.Vertical, VerticalOptions = LayoutOptions.FillAndExpand, Padding = new Thickness(0, 25, 0, 0) }; string requiredParticipationPercentage = Math.Round(protocol.RewardThreshold.GetValueOrDefault() * 100, 0) + "%"; string participationPercentage = Math.Round(participationRewardDatum.Participation * 100, 0) + "%"; if (protocol.RewardThreshold == null) { contentLayout.Children.Add( new Label { Text = "Verified Participation Level", FontSize = 20, HorizontalOptions = LayoutOptions.CenterAndExpand }); contentLayout.Children.Add( new Label { Text = participationPercentage, FontSize = 50, HorizontalOptions = LayoutOptions.CenterAndExpand } ); } else { bool reward = participationRewardDatum.Participation >= protocol.RewardThreshold.GetValueOrDefault(); contentLayout.Children.Add( new Image { HorizontalOptions = LayoutOptions.FillAndExpand, Source = ImageSource.FromFile(reward ? "check.png" : "x.png") }); contentLayout.Children.Add( new Label { Text = "Participant should " + (reward ? "" : "not ") + "be rewarded. This study requires " + requiredParticipationPercentage + " participation, and the participant is at " + participationPercentage + ".", FontSize = 20, HorizontalOptions = LayoutOptions.CenterAndExpand }); } Content = new ScrollView { Content = contentLayout }; }
public ParticipationReportPage(Protocol protocol, ParticipationRewardDatum participationRewardDatum, bool displayDatumQrCode) { Title = protocol.Name; #if __IOS__ string howToIncreaseScore = "You can increase your score by opening Sensus more often and responding to questions that Sensus asks you."; #elif __ANDROID__ string howToIncreaseScore = "You can increase your score by allowing Sensus to run continuously and responding to questions that Sensus asks you."; #elif WINDOWS_PHONE string userNotificationMessage = null; // TODO: How to increase score? #else #error "Unrecognized platform." #endif StackLayout contentLayout = new StackLayout { Orientation = StackOrientation.Vertical, VerticalOptions = LayoutOptions.FillAndExpand, Padding = new Thickness(0, 25, 0, 0), Children = { new Label { Text = "Participation Level", FontSize = 20, HorizontalOptions = LayoutOptions.CenterAndExpand }, new Label { Text = Math.Round(participationRewardDatum.Participation * 100, 0) + "%", FontSize = 50, HorizontalOptions = LayoutOptions.CenterAndExpand }, new Label { Text = "This score reflects your participation level over the past " + (protocol.ParticipationHorizonDays == 1 ? "day" : protocol.ParticipationHorizonDays + " days") + "." + (displayDatumQrCode ? " Anyone can verify your participation by tapping \"Scan Participation Barcode\" on their device and scanning the following barcode:" : ""), FontSize = 20, HorizontalOptions = LayoutOptions.CenterAndExpand } } }; if (displayDatumQrCode) { Label expirationLabel = new Label { FontSize = 15, HorizontalOptions = LayoutOptions.CenterAndExpand }; contentLayout.Children.Add(expirationLabel); Timer timer = new Timer(1000); timer.Elapsed += (o, e) => { Device.BeginInvokeOnMainThread(() => { int secondsLeftBeforeBarcodeExpiration = (int)(SensusServiceHelper.PARTICIPATION_VERIFICATION_TIMEOUT_SECONDS - (DateTimeOffset.UtcNow - participationRewardDatum.Timestamp).TotalSeconds); if (secondsLeftBeforeBarcodeExpiration <= 0) { expirationLabel.TextColor = Color.Red; expirationLabel.Text = "Barcode has expired. Please reopen this page to renew it."; timer.Stop(); } else { --secondsLeftBeforeBarcodeExpiration; expirationLabel.Text = "Barcode will expire in " + secondsLeftBeforeBarcodeExpiration + " second" + (secondsLeftBeforeBarcodeExpiration == 1 ? "" : "s") + "."; } }); }; timer.Start(); Disappearing += (o, e) => { timer.Stop(); }; contentLayout.Children.Add(new Image { Source = SensusServiceHelper.Get().GetQrCodeImageSource(protocol.RemoteDataStore.GetDatumKey(participationRewardDatum)), HorizontalOptions = LayoutOptions.CenterAndExpand }); } contentLayout.Children.Add(new Label { Text = howToIncreaseScore, FontSize = 20, HorizontalOptions = LayoutOptions.CenterAndExpand }); if (!string.IsNullOrWhiteSpace(protocol.ContactEmail)) { Button emailStudyManagerButton = new Button { Text = "Email Study Manager", FontSize = 20 }; emailStudyManagerButton.Clicked += (o, e) => { SensusServiceHelper.Get().SendEmailAsync(protocol.ContactEmail, "Help with Sensus study: " + protocol.Name, "Hello - " + Environment.NewLine + Environment.NewLine + "I am having trouble with a Sensus study. The name of the study is \"" + protocol.Name + "\"." + Environment.NewLine + Environment.NewLine + "Here is why I am sending this email: "); }; contentLayout.Children.Add(emailStudyManagerButton); } Button viewParticipationDetailsButton = new Button { Text = "View Participation Details", FontSize = 20 }; viewParticipationDetailsButton.Clicked += async (o, e) => { await Navigation.PushAsync(new ParticipationReportDetailsPage(protocol)); }; contentLayout.Children.Add(viewParticipationDetailsButton); Content = new ScrollView { Content = contentLayout }; }
public ProtocolsPage() { Title = "Your Sensus Studies"; _protocolsList = new ListView(); _protocolsList.ItemTemplate = new DataTemplate(typeof(TextCell)); _protocolsList.ItemTemplate.SetBinding(TextCell.TextProperty, new Binding(".", converter: new ProtocolNameValueConverter())); _protocolsList.ItemTemplate.SetBinding(TextCell.TextColorProperty, new Binding(".", converter: new ProtocolColorValueConverter())); _protocolsList.ItemTapped += async (o, e) => { if (_protocolsList.SelectedItem == null) return; Protocol selectedProtocol = _protocolsList.SelectedItem as Protocol; List<string> actions = new List<string>(); actions.Add(selectedProtocol.Running ? "Stop" : "Start"); if (selectedProtocol.Running) actions.Add("Display Participation"); actions.AddRange(new string[] { "Scan Participation Barcode", "Edit", "Copy", "Share" }); List<Protocol> groupableProtocols = SensusServiceHelper.Get().RegisteredProtocols.Where(registeredProtocol => registeredProtocol != selectedProtocol && registeredProtocol.Groupable && registeredProtocol.GroupedProtocols.Count == 0).ToList(); if (selectedProtocol.Groupable) { if (selectedProtocol.GroupedProtocols.Count == 0 && groupableProtocols.Count > 0) actions.Add("Group"); else if (selectedProtocol.GroupedProtocols.Count > 0) actions.Add("Ungroup"); } if (selectedProtocol.Running) actions.Add("Status"); actions.Add("Delete"); string selectedAction = await DisplayActionSheet(selectedProtocol.Name, "Cancel", null, actions.ToArray()); // must reset the protocol select manually Device.BeginInvokeOnMainThread(() => { _protocolsList.SelectedItem = null; }); if (selectedAction == "Start") { selectedProtocol.StartWithUserAgreementAsync(null, () => { // rebind to pick up color and running status changes Device.BeginInvokeOnMainThread(Bind); }); } else if (selectedAction == "Stop") { if (await DisplayAlert("Confirm Stop", "Are you sure you want to stop " + selectedProtocol.Name + "?", "Yes", "No")) { selectedProtocol.StopAsync(() => { // rebind to pick up color and running status changes Device.BeginInvokeOnMainThread(Bind); }); } } else if (selectedAction == "Display Participation") { CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); // pop up wait screen while we submit the participation reward datum SensusServiceHelper.Get().PromptForInputsAsync( null, new InputGroup[] { new InputGroup("Please Wait", new LabelOnlyInput("Submitting participation information.", false)) }, cancellationTokenSource.Token, false, "Cancel", null, null, null, false, async () => { // add participation reward datum to remote data store and commit immediately ParticipationRewardDatum participationRewardDatum = new ParticipationRewardDatum(DateTimeOffset.UtcNow, selectedProtocol.Participation); selectedProtocol.RemoteDataStore.AddNonProbeDatum(participationRewardDatum); bool commitFailed; try { await selectedProtocol.RemoteDataStore.CommitAsync(cancellationTokenSource.Token); // we should not have any remaining non-probe data commitFailed = selectedProtocol.RemoteDataStore.HasNonProbeDatumToCommit(participationRewardDatum.Id); } catch (Exception) { commitFailed = true; } if (commitFailed) SensusServiceHelper.Get().FlashNotificationAsync("Failed to submit participation information to remote server. You will not be able to verify your participation at this time."); // cancel the token to close the input above, but only if the token hasn't already been canceled. if (!cancellationTokenSource.IsCancellationRequested) cancellationTokenSource.Cancel(); Device.BeginInvokeOnMainThread(async() => { // only show the QR code for the reward datum if the datum was committed to the remote data store await Navigation.PushAsync(new ParticipationReportPage(selectedProtocol, participationRewardDatum, !commitFailed)); }); }, inputs => { // if the prompt was closed by the user instead of the cancellation token, cancel the token in order // to cancel the remote data store commit. if the prompt was closed by the termination of the remote // data store commit (i.e., by the canceled token), then don't cancel the token again. if (!cancellationTokenSource.IsCancellationRequested) cancellationTokenSource.Cancel(); }); } else if (selectedAction == "Scan Participation Barcode") { Result barcodeResult = null; try { if (await SensusServiceHelper.Get().ObtainPermissionAsync(Permission.Camera) != PermissionStatus.Granted) throw new Exception("Could not access camera."); ZXing.Mobile.MobileBarcodeScanner scanner = SensusServiceHelper.Get().BarcodeScanner; if (scanner == null) throw new Exception("Barcode scanner not present."); scanner.TopText = "Position a Sensus participation barcode in the window below, with the red line across the middle of the barcode."; scanner.BottomText = "Sensus is not recording any of these images. Sensus is only trying to find a barcode."; scanner.CameraUnsupportedMessage = "There is not a supported camera on this phone. Cannot scan barcode."; barcodeResult = await scanner.Scan(new ZXing.Mobile.MobileBarcodeScanningOptions { PossibleFormats = new BarcodeFormat[] { BarcodeFormat.QR_CODE }.ToList() }); } catch (Exception ex) { string message = "Failed to scan barcode: " + ex.Message; SensusServiceHelper.Get().Logger.Log(message, LoggingLevel.Normal, GetType()); SensusServiceHelper.Get().FlashNotificationAsync(message); } if (barcodeResult != null) { CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); // pop up wait screen while we get the participation reward datum SensusServiceHelper.Get().PromptForInputsAsync( null, new InputGroup[] { new InputGroup("Please Wait", new LabelOnlyInput("Retrieving participation information.", false)) }, cancellationTokenSource.Token, false, "Cancel", null, null, null, false, async () => { try { ParticipationRewardDatum participationRewardDatum = await selectedProtocol.RemoteDataStore.GetDatum<ParticipationRewardDatum>(barcodeResult.Text, cancellationTokenSource.Token); // cancel the token to close the input above, but only if the token hasn't already been canceled. if (!cancellationTokenSource.IsCancellationRequested) cancellationTokenSource.Cancel(); // ensure that the participation datum has not expired if (participationRewardDatum.Timestamp > DateTimeOffset.UtcNow.AddSeconds(-SensusServiceHelper.PARTICIPATION_VERIFICATION_TIMEOUT_SECONDS)) { Device.BeginInvokeOnMainThread(async() => { await Navigation.PushAsync(new VerifiedParticipationPage(selectedProtocol, participationRewardDatum)); }); } else SensusServiceHelper.Get().FlashNotificationAsync("Participation barcode has expired. The participant needs to regenerate the barcode."); } catch (Exception) { SensusServiceHelper.Get().FlashNotificationAsync("Failed to retrieve participation information."); } finally { // cancel the token to close the input above, but only if the token hasn't already been canceled. this will be // used if an exception is thrown while getting the participation reward datum. if (!cancellationTokenSource.IsCancellationRequested) cancellationTokenSource.Cancel(); } }, inputs => { // if the prompt was closed by the user instead of the cancellation token, cancel the token in order // to cancel the datum retrieval. if the prompt was closed by the termination of the remote // data store get (i.e., by the canceled token), then don't cancel the token again. if (!cancellationTokenSource.IsCancellationRequested) cancellationTokenSource.Cancel(); }); } } else if (selectedAction == "Edit") { ExecuteActionUponProtocolAuthentication(selectedProtocol, () => { Device.BeginInvokeOnMainThread(async () => { ProtocolPage protocolPage = new ProtocolPage(selectedProtocol); protocolPage.Disappearing += (oo, ee) => Bind(); // rebind to pick up name changes await Navigation.PushAsync(protocolPage); }); } ); } else if (selectedAction == "Copy") selectedProtocol.CopyAsync(true, true); else if (selectedAction == "Share") { Action ShareSelectedProtocol = new Action(() => { // make a deep copy of the selected protocol so we can reset it for sharing selectedProtocol.CopyAsync(false, false, selectedProtocolCopy => { selectedProtocolCopy.ResetForSharing(); // write protocol to file and share string sharePath = SensusServiceHelper.Get().GetSharePath(".json"); selectedProtocolCopy.Save(sharePath); SensusServiceHelper.Get().ShareFileAsync(sharePath, "Sensus Protocol: " + selectedProtocolCopy.Name, "application/json"); }); }); if (selectedProtocol.Shareable) ShareSelectedProtocol(); else ExecuteActionUponProtocolAuthentication(selectedProtocol, ShareSelectedProtocol); } else if (selectedAction == "Group") { SensusServiceHelper.Get().PromptForInputAsync("Group", new ItemPickerPageInput("Select Protocols", groupableProtocols.Cast<object>().ToList(), "Name") { Multiselect = true }, null, true, "Group", null, null, null, false, input => { if (input == null) { SensusServiceHelper.Get().FlashNotificationAsync("No protocols grouped."); return; } ItemPickerPageInput itemPickerPageInput = input as ItemPickerPageInput; List<Protocol> selectedProtocols = (itemPickerPageInput.Value as List<object>).Cast<Protocol>().ToList(); if (selectedProtocols.Count == 0) SensusServiceHelper.Get().FlashNotificationAsync("No protocols grouped."); else { selectedProtocol.GroupedProtocols.AddRange(selectedProtocols); SensusServiceHelper.Get().FlashNotificationAsync("Grouped \"" + selectedProtocol.Name + "\" with " + selectedProtocols.Count + " other protocol" + (selectedProtocols.Count == 1 ? "" : "s") + "."); } }); } else if (selectedAction == "Ungroup") { if (await DisplayAlert("Ungroup " + selectedProtocol.Name + "?", "This protocol is currently grouped with the following other protocols:" + Environment.NewLine + Environment.NewLine + string.Concat(selectedProtocol.GroupedProtocols.Select(protocol => protocol.Name + Environment.NewLine)), "Ungroup", "Cancel")) selectedProtocol.GroupedProtocols.Clear(); } else if (selectedAction == "Status") { if (SensusServiceHelper.Get().ProtocolShouldBeRunning(selectedProtocol)) { selectedProtocol.TestHealthAsync(true, () => { Device.BeginInvokeOnMainThread(async () => { if (selectedProtocol.MostRecentReport == null) await DisplayAlert("No Report", "Status check failed.", "OK"); else await Navigation.PushAsync(new ViewTextLinesPage("Protocol Status", selectedProtocol.MostRecentReport.ToString().Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries).ToList(), null, null)); }); }); } else await DisplayAlert("Protocol Not Running", "Cannot check status of protocol when protocol is not running.", "OK"); } else if (selectedAction == "Delete") { if (await DisplayAlert("Delete " + selectedProtocol.Name + "?", "This action cannot be undone.", "Delete", "Cancel")) selectedProtocol.DeleteAsync(); } }; Content = _protocolsList; ToolbarItems.Add(new ToolbarItem(null, "gear_wrench.png", async () => { double shareDirectoryMB = SensusServiceHelper.GetDirectorySizeMB(SensusServiceHelper.SHARE_DIRECTORY); string clearShareDirectoryAction = "Clear Share Directory (" + Math.Round(shareDirectoryMB, 1) + " MB)"; List<string> buttons = new string[] { "New Protocol", "View Log", "View Points of Interest", clearShareDirectoryAction }.ToList(); // stopping only makes sense on android, where we use a background service. on ios, there is no concept // of stopping the app other than the user or system terminating the app. #if __ANDROID__ buttons.Add("Stop Sensus"); #endif string action = await DisplayActionSheet("Other Actions", "Back", null, buttons.ToArray()); if (action == "New Protocol") Protocol.CreateAsync("New Protocol", null); else if (action == "View Log") { await Navigation.PushAsync(new ViewTextLinesPage("Log", SensusServiceHelper.Get().Logger.Read(200, true), () => { string sharePath = null; try { sharePath = SensusServiceHelper.Get().GetSharePath(".txt"); SensusServiceHelper.Get().Logger.CopyTo(sharePath); } catch (Exception) { sharePath = null; } if (sharePath != null) SensusServiceHelper.Get().ShareFileAsync(sharePath, "Log: " + Path.GetFileName(sharePath), "text/plain"); }, () => SensusServiceHelper.Get().Logger.Clear())); } else if (action == "View Points of Interest") await Navigation.PushAsync(new PointsOfInterestPage(SensusServiceHelper.Get().PointsOfInterest)); else if (action == clearShareDirectoryAction) { foreach (string sharePath in Directory.GetFiles(SensusServiceHelper.SHARE_DIRECTORY)) { try { File.Delete(sharePath); } catch (Exception ex) { string errorMessage = "Failed to delete shared file \"" + Path.GetFileName(sharePath) + "\": " + ex.Message; SensusServiceHelper.Get().FlashNotificationAsync(errorMessage); SensusServiceHelper.Get().Logger.Log(errorMessage, LoggingLevel.Normal, GetType()); } } } #if __ANDROID__ else if (action == "Stop Sensus" && await DisplayAlert("Confirm", "Are you sure you want to stop Sensus? This will end your participation in all studies.", "Stop Sensus", "Go Back")) SensusServiceHelper.Get().Stop(); #endif })); }