// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Threading.Tasks; using Windows.ApplicationModel.ExtendedExecution; using Windows.Devices.Geolocation; using Windows.Foundation; using Windows.Storage.Streams; using Windows.UI; using Windows.UI.Core; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls.Maps; using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Media.Imaging; using Windows.UI.Xaml.Navigation; using MyDriving.ViewModel; using MyDriving.Utils; using Windows.Devices.Enumeration; using Windows.Devices.Bluetooth.Rfcomm; namespace MyDriving.UWP.Views { /// /// An empty page that can be used on its own or navigated to within a Frame. /// public sealed partial class CurrentTripView : INotifyPropertyChanged { private bool recordButtonIsBusy = false; private ImageSource recordButtonImage; private ExtendedExecutionSession session; public CurrentTripViewModel ViewModel; public CurrentTripView() { InitializeComponent(); ViewModel = new CurrentTripViewModel(); Locations = new List(); if (Logger.BingMapsAPIKey != "Ar6iuHZYgX1BrfJs6SRJaXWbpU_HKdoe7G-OO9b2kl3rWvcawYx235GGx5FPM76O") { MyMap.MapServiceToken = Logger.BingMapsAPIKey; } MyMap.Loaded += MyMap_Loaded; DataContext = this; recordButtonImage = new BitmapImage(new Uri("ms-appx:///Assets/StartRecord.png", UriKind.Absolute)); OnPropertyChanged(nameof(RecordButtonImage)); StartRecordBtn.Click += StartRecordBtn_Click; } public IList Locations { get; set; } public ImageSource RecordButtonImage => recordButtonImage; public event PropertyChangedEventHandler PropertyChanged; //private Geolocator geolocator = null; public void OnPropertyChanged(string name) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); } private void MyMap_Loaded(object sender, RoutedEventArgs e) { MyMap.ZoomLevel = 16; MyMap.MapElements.Clear(); } protected override async void OnNavigatedTo(NavigationEventArgs e) { base.OnNavigatedTo(e); App.SetTitle("CURRENT TRIP"); ViewModel.PropertyChanged += OnPropertyChanged; await StartTrackingAsync(); UpdateStats(); SystemNavigationManager systemNavigationManager = SystemNavigationManager.GetForCurrentView(); systemNavigationManager.BackRequested += SystemNavigationManager_BackRequested; } protected override void OnNavigatedFrom(NavigationEventArgs e) { base.OnNavigatedFrom(e); //Ideally, we should stop tracking only if we aren't recording ViewModel.StopTrackingTripCommand.Execute(null); Locations?.Clear(); Locations = null; MyMap.MapElements.Clear(); MyMap.Loaded -= MyMap_Loaded; StartRecordBtn.Click -= StartRecordBtn_Click; ViewModel.PropertyChanged -= OnPropertyChanged; SystemNavigationManager systemNavigationManager = SystemNavigationManager.GetForCurrentView(); systemNavigationManager.BackRequested -= SystemNavigationManager_BackRequested; ClearExtendedExecution(); } private void SystemNavigationManager_BackRequested(object sender, BackRequestedEventArgs e) { if (!e.Handled) { e.Handled = TryGoBack(); } } private bool TryGoBack() { bool navigated = false; if (Frame.CanGoBack && !ViewModel.IsRecording) { Frame.GoBack(); navigated = true; } return navigated; } void OnPropertyChanged(object sender, PropertyChangedEventArgs e) { switch (e.PropertyName) { case nameof(ViewModel.CurrentPosition): var basicGeoposition = new BasicGeoposition() { Latitude = ViewModel.CurrentPosition.Latitude, Longitude = ViewModel.CurrentPosition.Longitude }; UpdateMap_PositionChanged(basicGeoposition); UpdateStats(); break; case nameof(ViewModel.CurrentTrip): ResetTrip(); break; // Todo VJ. Fix Databinding issue to directly update the UI. Currently updating manually. case nameof(ViewModel.Distance): case nameof(ViewModel.EngineLoad): case nameof(ViewModel.FuelConsumption): case nameof(ViewModel.ElapsedTime): case nameof(ViewModel.DistanceUnits): case nameof(ViewModel.FuelConsumptionUnits): UpdateStats(); break; } } private async Task StartTrackingAsync() { // Request permission to access bluetooth try { DeviceInformationCollection deviceInfoCollection = await DeviceInformation.FindAllAsync(RfcommDeviceService.GetDeviceSelector(RfcommServiceId.SerialPort)); if (deviceInfoCollection.Count() > 0) { DeviceInformation device = deviceInfoCollection[0]; await RfcommDeviceService.FromIdAsync(device.Id); } } catch (Exception) { } // Request permission to access location var accessStatus = await Geolocator.RequestAccessAsync(); switch (accessStatus) { case GeolocationAccessStatus.Allowed: StartRecordBtn.IsEnabled = true; await BeginExtendedExecution(); break; case GeolocationAccessStatus.Denied: Acr.UserDialogs.UserDialogs.Instance.Alert( "Please ensure that geolocation is enabled and permissions are allowed for MyDriving to start a recording.", "Geolocation Disabled", "OK"); StartRecordBtn.IsEnabled = false; break; case GeolocationAccessStatus.Unspecified: Acr.UserDialogs.UserDialogs.Instance.Alert("Unspecified Error...", "Geolocation Disabled", "OK"); StartRecordBtn.IsEnabled = false; break; } } private async Task BeginExtendedExecution() { if (ViewModel == null) return; ClearExtendedExecution(); try { var newSession = new ExtendedExecutionSession { Reason = ExtendedExecutionReason.LocationTracking, Description = "Tracking your location" }; newSession.Revoked += SessionRevoked; ExtendedExecutionResult result = await newSession.RequestExtensionAsync(); switch (result) { case ExtendedExecutionResult.Allowed: session = newSession; ViewModel.Geolocator.AllowsBackgroundUpdates = true; ViewModel.StartTrackingTripCommand.Execute(null); break; default: Acr.UserDialogs.UserDialogs.Instance.Alert("Unable to execute app in the background.", "Background execution denied.", "OK"); newSession.Dispose(); break; } } catch (Exception ex) { // Sometimes while creating ExtendedExecution session you get Resource not ready exception. Logger.Instance.Report(ex); Acr.UserDialogs.UserDialogs.Instance.Alert("Will not be able to execute app in the background.", "Background execution session failed.", "OK"); } } private void ClearExtendedExecution() { if (session != null) { session.Revoked -= SessionRevoked; session.Dispose(); session = null; } } private async void SessionRevoked(object sender, ExtendedExecutionRevokedEventArgs args) { await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, async () => { switch (args.Reason) { case ExtendedExecutionRevokedReason.Resumed: break; case ExtendedExecutionRevokedReason.SystemPolicy: Acr.UserDialogs.UserDialogs.Instance.Alert("Extended execution revoked due to system policy.", "Background Execution revoked.", "OK"); break; } ClearExtendedExecution(); await BeginExtendedExecution(); }); } private async void StartRecordBtn_Click(object sender, RoutedEventArgs e) { if (ViewModel?.CurrentPosition == null || ViewModel.IsBusy || recordButtonIsBusy) return; recordButtonIsBusy = true; var basicGeoposition = new BasicGeoposition() { Latitude = ViewModel.CurrentPosition.Latitude, Longitude = ViewModel.CurrentPosition.Longitude }; if (ViewModel.IsRecording) { // Need to update Map UI before we start saving. So that the entire trip is visible. UpdateMap_PositionChanged(basicGeoposition); // Changing the record button icon and andding end marker earlier to notify user about the stop process. recordButtonImage = new BitmapImage(new Uri("ms-appx:///Assets/StartRecord.png", UriKind.Absolute)); OnPropertyChanged(nameof(RecordButtonImage)); AddEndMarker(basicGeoposition); if (!await ViewModel.StopRecordingTrip()) { recordButtonIsBusy = false; return; } await ViewModel.SaveRecordingTripAsync(); var recordedTripSummary = ViewModel.TripSummary; // Launch Trip Summary Page. Frame.Navigate(typeof(TripSummaryView), recordedTripSummary); } else { if (!await ViewModel.StartRecordingTrip()) { recordButtonIsBusy = false; return; } if (ViewModel.CurrentTrip.HasSimulatedOBDData) App.SetTitle("CURRENT TRIP (SIMULATED OBD)"); // Update UI to start recording. recordButtonImage = new BitmapImage(new Uri("ms-appx:///Assets/StopRecord.png", UriKind.Absolute)); OnPropertyChanged(nameof(RecordButtonImage)); // Update the Map with StartMarker, Path AddStartMarker(basicGeoposition); UpdateMap_PositionChanged(basicGeoposition); UpdateStats(); } recordButtonIsBusy = false; } private async void UpdateMap_PositionChanged(BasicGeoposition basicGeoposition) { if (ViewModel.IsBusy) return; // To update the carIcon first find it if it exists in MapElements already. // If it exists just update the existing one with new location and image await Dispatcher.RunAsync(CoreDispatcherPriority.Low, () => { MapIcon _carIcon = null; // Find if there is a MapIcon with title Car if (MyMap.MapElements != null) { var mapIcons = MyMap.MapElements.OfType().ToList(); foreach (var item in mapIcons) { if (item.Title == "Car") _carIcon = item; } } if (_carIcon == null) { // Car Icon is currently not present. So add it. _carIcon = new MapIcon { NormalizedAnchorPoint = new Point(0.5, 0.5), ZIndex = 4, CollisionBehaviorDesired = MapElementCollisionBehavior.RemainVisible, Title = "Car" }; MyMap.MapElements.Add(_carIcon); } // Update the icon of the car based on the recording status if (ViewModel.IsRecording) _carIcon.Image = RandomAccessStreamReference.CreateFromUri(new Uri("ms-appx:///Assets/RedCar.png")); else _carIcon.Image = RandomAccessStreamReference.CreateFromUri(new Uri("ms-appx:///Assets/BlueCar.png")); // Update the location _carIcon.Location = new Geopoint(basicGeoposition); MyMap.Center = _carIcon.Location; // Add Path if we are recording DrawPath(basicGeoposition); }); } private async void AddStartMarker(BasicGeoposition basicGeoposition) { if (!ViewModel.IsRecording || ViewModel.CurrentTrip.Points.Count == 0) return; await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { // First point of the trip will be Start Position. MapIcon mapStartIcon = new MapIcon { Location = new Geopoint(basicGeoposition), NormalizedAnchorPoint = new Point(0.5, 0.5), Image = RandomAccessStreamReference.CreateFromUri(new Uri("ms-appx:///Assets/A100.png")), ZIndex = 3, CollisionBehaviorDesired = MapElementCollisionBehavior.RemainVisible }; MyMap.MapElements.Add(mapStartIcon); }); } private async void DrawPath(BasicGeoposition basicGeoposition) { if (!ViewModel.IsRecording || ViewModel.CurrentTrip?.Points?.Count == 0) return; await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { if (MyMap == null) return; if (Locations.Count == 0) { Locations = new List( ViewModel.CurrentTrip?.Points?.Select( s => new BasicGeoposition() { Latitude = s.Latitude, Longitude = s.Longitude })); // If the viewmodel still has not added this point, then add it locally to the Locations collection. if (Locations.Count == 0) Locations.Add(basicGeoposition); } else Locations.Add(basicGeoposition); // Check if _mapPolyline is already in MapElements var _mapPolyline = MyMap.MapElements.OfType().FirstOrDefault(); if (_mapPolyline == null) { // Polyline does not exist. Create a new path and add it. _mapPolyline = new MapPolyline { StrokeColor = Colors.Red, StrokeThickness = 3, Visible = true, Path = new Geopath(Locations) }; MyMap.MapElements.Add(_mapPolyline); } else { // Set the path of the already added polyline to new locations _mapPolyline.Path = new Geopath(Locations); } }); } private async void UpdateMapView(BasicGeoposition basicGeoposition) { var geoPoint = new Geopoint(basicGeoposition); if (!ViewModel.IsBusy) { await MyMap.TrySetViewAsync(geoPoint); } } private async void UpdateStats() { await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { TextFuel.Text = ViewModel.FuelConsumption; TextFuelunits.Text = ViewModel.FuelConsumptionUnits; TextDistance.Text = ViewModel.Distance; TextDistanceunits.Text = ViewModel.DistanceUnits; TextTime.Text = ViewModel.ElapsedTime; TextEngineload.Text = ViewModel.EngineLoad; }); } private void ResetTrip() { MyMap.MapElements.Clear(); Locations?.Clear(); Locations = null; UpdateStats(); } private async void AddEndMarker(BasicGeoposition basicGeoposition) { await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { MapIcon mapEndIcon = new MapIcon { Location = new Geopoint(basicGeoposition), NormalizedAnchorPoint = new Point(0.5, 0.5), Image = RandomAccessStreamReference.CreateFromUri(new Uri("ms-appx:///Assets/B100.png")), ZIndex = 3, CollisionBehaviorDesired = MapElementCollisionBehavior.RemainVisible }; MyMap.MapElements.Add(mapEndIcon); }); } } }