// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. using Android.OS; using Android.Support.V4.Content; using Android.Views; using Android.Widget; using MyDriving.Droid.Services; using MyDriving.ViewModel; using MvvmHelpers; using MyDriving.DataObjects; using Android.Gms.Maps; using Android.Gms.Maps.Model; using System; using Android.Graphics; using Android.Graphics.Drawables; using Plugin.Permissions; using Plugin.Permissions.Abstractions; using System.Threading.Tasks; using MyDriving.Droid.Activities; using MyDriving.Utils; using MyDriving.Droid.Helpers; using Android.Support.Design.Widget; using System.Collections.Specialized; using System.Collections.Generic; using Android.Views.Animations; using Android.Animation; namespace MyDriving.Droid.Fragments { public class FragmentCurrentTrip : Android.Support.V4.App.Fragment, IOnMapReadyCallback { List allPoints; Marker carMarker; TextView distance, distanceUnits, time, load, consumption, consumptionUnits; Polyline driveLine; Color? driveLineColor; FloatingActionButton fab; GoogleMap map; MapView mapView; bool setZoom = true; LinearLayout stats; ObservableRangeCollection trailPointList; CurrentTripViewModel viewModel; public void OnMapReady(GoogleMap googleMap) { map = googleMap; if (viewModel != null) SetupMap(); } public static FragmentCurrentTrip NewInstance() => new FragmentCurrentTrip {Arguments = new Bundle()}; public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { base.OnCreateView(inflater, container, savedInstanceState); HasOptionsMenu = true; var view = inflater.Inflate(Resource.Layout.fragment_current_trip, null); mapView = view.FindViewById(Resource.Id.map); mapView.OnCreate(savedInstanceState); fab = view.FindViewById(Resource.Id.fab); time = view.FindViewById(Resource.Id.text_time); distance = view.FindViewById(Resource.Id.text_distance); distanceUnits = view.FindViewById(Resource.Id.text_distance_units); consumption = view.FindViewById(Resource.Id.text_consumption); consumptionUnits = view.FindViewById(Resource.Id.text_consumption_units); load = view.FindViewById(Resource.Id.text_load); stats = view.FindViewById(Resource.Id.stats); stats.Visibility = ViewStates.Invisible; return view; } public override void OnActivityCreated(Bundle savedInstanceState) { mapView.GetMapAsync(this); base.OnActivityCreated(savedInstanceState); } void StartFadeAnimation(bool fadeIn) { //handle first run if (!viewModel.IsRecording && stats.Visibility == ViewStates.Invisible) return; var start = fadeIn ? 0f : 1f; var end = fadeIn ? 1f : 0f; stats.Alpha = fadeIn ? 0f : 1f; stats.Visibility = ViewStates.Visible; var timerAnimator = ValueAnimator.OfFloat(start, end); timerAnimator.SetDuration(Java.Util.Concurrent.TimeUnit.Seconds.ToMillis(1)); timerAnimator.SetInterpolator(new AccelerateInterpolator()); timerAnimator.Update += (sender, e) => { Activity.RunOnUiThread(() => stats.Alpha = (float) e.Animation.AnimatedValue); }; timerAnimator.Start(); } void OnLocationServiceConnected(object sender, ServiceConnectedEventArgs e) { viewModel = GeolocationHelper.Current.LocationService.ViewModel; viewModel.PropertyChanged += OnPropertyChanged; ResetTrip(); } void OnPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) { switch (e.PropertyName) { case nameof(viewModel.CurrentPosition): var latlng = viewModel.CurrentPosition.ToLatLng(); UpdateCar(latlng); UpdateCamera(latlng); break; case nameof(viewModel.CurrentTrip): TripSummaryActivity.ViewModel = viewModel.TripSummary; ResetTrip(); StartActivity(new Android.Content.Intent(Activity, typeof (TripSummaryActivity))); break; case "Stats": UpdateStats(); break; } } void UpdateStats() { Activity?.RunOnUiThread(() => { time.Text = viewModel.ElapsedTime; consumption.Text = viewModel.FuelConsumption; consumptionUnits.Text = viewModel.FuelConsumptionUnits; load.Text = viewModel.EngineLoad; distanceUnits.Text = viewModel.DistanceUnits; distance.Text = viewModel.CurrentTrip.TotalDistanceNoUnits; }); } void ResetTrip() { trailPointList = viewModel.CurrentTrip.Points as ObservableRangeCollection; trailPointList.CollectionChanged += OnTrailUpdated; carMarker = null; map?.Clear(); allPoints?.Clear(); allPoints = null; SetupMap(); UpdateStats(); StartFadeAnimation(viewModel.IsRecording); Activity.SupportInvalidateOptionsMenu(); } void AddStartMarker(LatLng start) { Activity?.RunOnUiThread(() => { var logicalDensity = Resources.DisplayMetrics.Density; var thicknessPoints = (int) Math.Ceiling(20*logicalDensity + .5f); var b = ContextCompat.GetDrawable(Activity, Resource.Drawable.ic_start_point) as BitmapDrawable; var finalIcon = Bitmap.CreateScaledBitmap(b.Bitmap, thicknessPoints, thicknessPoints, false); var startMarker = new MarkerOptions(); startMarker.SetPosition(start); startMarker.SetIcon(BitmapDescriptorFactory.FromBitmap(finalIcon)); startMarker.Anchor(.5f, .5f); map.AddMarker(startMarker); }); } void AddEndMarker(LatLng end) { Activity?.RunOnUiThread(() => { var logicalDensity = Resources.DisplayMetrics.Density; var thicknessPoints = (int) Math.Ceiling(20*logicalDensity + .5f); var b = ContextCompat.GetDrawable(Activity, Resource.Drawable.ic_end_point) as BitmapDrawable; var finalIcon = Bitmap.CreateScaledBitmap(b.Bitmap, thicknessPoints, thicknessPoints, false); var endMarker = new MarkerOptions(); endMarker.SetPosition(end); endMarker.SetIcon(BitmapDescriptorFactory.FromBitmap(finalIcon)); endMarker.Anchor(.5f, .5f); map.AddMarker(endMarker); }); } void OnTrailUpdated(object sender, NotifyCollectionChangedEventArgs e) { Activity?.RunOnUiThread(() => { var item = viewModel.CurrentTrip.Points[viewModel.CurrentTrip.Points.Count - 1]; if (carMarker != null) UpdateMap(item); else SetupMap(); }); } void UpdateCarIcon(bool recording) { Activity?.RunOnUiThread(() => { var logicalDensity = Resources.DisplayMetrics.Density; var thicknessCar = (int) Math.Ceiling(26*logicalDensity + .5f); var b = ContextCompat.GetDrawable(Activity, recording ? Resource.Drawable.ic_car_red : Resource.Drawable.ic_car_blue) as BitmapDrawable; //var b = ContextCompat.GetDrawable(Activity, Resource.Drawable.ic_car) as BitmapDrawable; var finalIcon = Bitmap.CreateScaledBitmap(b.Bitmap, thicknessCar, thicknessCar, false); carMarker?.SetIcon(BitmapDescriptorFactory.FromBitmap(finalIcon)); fab.SetImageResource(recording ? Resource.Drawable.ic_stop : Resource.Drawable.ic_start); }); } void SetupMap() { if (map == null) return; if (mapView.Width == 0) { mapView.PostDelayed(SetupMap, 500); return; } if (viewModel.CurrentTrip.HasSimulatedOBDData) { var activity = (BaseActivity) Activity; activity.SupportActionBar.Title = "Current Trip (Sim OBD)"; } TripPoint start = null; if (viewModel.CurrentTrip.Points.Count != 0) start = viewModel.CurrentTrip.Points[0]; UpdateMap(start, false); if (start != null) { UpdateCamera(carMarker.Position); AddStartMarker(start.ToLatLng()); } } void UpdateMap(TripPoint point, bool updateCamera = true) { if (map == null) return; //Get trail position or current potion to move car var latlng = point == null ? viewModel?.CurrentPosition?.ToLatLng() : point.ToLatLng(); Activity?.RunOnUiThread(() => { UpdateCar(latlng); driveLine?.Remove(); var polyOptions = new PolylineOptions(); if (allPoints == null) { allPoints = viewModel.CurrentTrip.Points.ToLatLngs(); } else if (point != null) { allPoints.Add(point.ToLatLng()); } polyOptions.Add(allPoints.ToArray()); if (!driveLineColor.HasValue) driveLineColor = new Color(ContextCompat.GetColor(Activity, Resource.Color.recording_accent)); polyOptions.InvokeColor(driveLineColor.Value); driveLine = map.AddPolyline(polyOptions); if (updateCamera) UpdateCamera(latlng); }); } void UpdateCar(LatLng latlng) { if (latlng == null || map == null) return; Activity?.RunOnUiThread(() => { if (carMarker == null) { var car = new MarkerOptions(); car.SetPosition(latlng); car.Anchor(.5f, .5f); carMarker = map.AddMarker(car); UpdateCarIcon(viewModel.IsRecording); return; } carMarker.Position = latlng; }); } void UpdateCamera(LatLng latlng) { if (map == null) return; Activity?.RunOnUiThread(() => { if (setZoom) { map.MoveCamera(CameraUpdateFactory.NewLatLngZoom(latlng, 14)); setZoom = false; } else { map.MoveCamera(CameraUpdateFactory.NewLatLng(latlng)); } }); } public override void OnStop() { base.OnStop(); GeolocationHelper.Current.LocationServiceConnected -= OnLocationServiceConnected; if (viewModel != null) viewModel.PropertyChanged -= OnPropertyChanged; if (trailPointList != null) trailPointList.CollectionChanged -= OnTrailUpdated; if (fab != null) fab.Click -= OnRecordButtonClick; //If we are recording then don't stop the background service if ((viewModel?.IsRecording).GetValueOrDefault()) return; GeolocationHelper.Current.LocationService.StopLocationUpdates(); GeolocationHelper.StopLocationService(); } public override async void OnStart() { base.OnStart(); GeolocationHelper.Current.LocationServiceConnected += OnLocationServiceConnected; if (fab != null) fab.Click += OnRecordButtonClick; await StartLocationService(); } async Task StartLocationService() { try { var status = await CrossPermissions.Current.CheckPermissionStatusAsync(Permission.Location); if (status != PermissionStatus.Granted) { var results = await CrossPermissions.Current.RequestPermissionsAsync(Permission.Location); status = results[Permission.Location]; } if (status == PermissionStatus.Granted) { if ((viewModel == null || !viewModel.IsRecording) && !GeolocationHelper.Current.IsRunning) await GeolocationHelper.StartLocationService(); else OnLocationServiceConnected(null, null); } else if (status != PermissionStatus.Unknown) { Toast.MakeText(Activity, "Location permission is not granted, can't track location", ToastLength.Long); } } catch (Exception ex) { Logger.Instance.Report(ex); } } #region Options Menu & User Actions public override void OnCreateOptionsMenu(IMenu menu, MenuInflater inflater) { //if((viewModel?.IsRecording).GetValueOrDefault()) // inflater.Inflate(Resource.Menu.menu_current_trip, menu); base.OnCreateOptionsMenu(menu, inflater); } public override bool OnOptionsItemSelected(IMenuItem item) { switch (item.ItemId) { case Resource.Id.menu_take_photo: if (!(viewModel?.IsBusy).GetValueOrDefault()) viewModel?.TakePhotoCommand.Execute(null); break; } return base.OnOptionsItemSelected(item); } async void OnRecordButtonClick(object sender, EventArgs e) { if (viewModel?.CurrentPosition == null || viewModel.IsBusy) return; if (viewModel.NeedSave) { await viewModel.SaveRecordingTripAsync(); } if (viewModel.IsRecording) { if (!await viewModel.StopRecordingTrip()) return; AddEndMarker(viewModel.CurrentPosition.ToLatLng()); UpdateCarIcon(false); var activity = (BaseActivity) Activity; activity.SupportActionBar.Title = "Current Trip"; await viewModel.SaveRecordingTripAsync(); } else { if (!await viewModel.StartRecordingTrip()) return; AddStartMarker(viewModel.CurrentPosition.ToLatLng()); Activity.SupportInvalidateOptionsMenu(); UpdateCarIcon(true); UpdateStats(); StartFadeAnimation(true); if (viewModel.CurrentTrip.HasSimulatedOBDData) { var activity = (BaseActivity) Activity; activity.SupportActionBar.Title = "Current Trip (Sim OBD)"; } } } #endregion #region MapView Lifecycle Events public override void OnResume() { base.OnResume(); mapView?.OnResume(); } public override void OnPause() { base.OnPause(); mapView?.OnPause(); } public override void OnDestroy() { base.OnDestroy(); mapView?.OnDestroy(); } public override void OnSaveInstanceState(Bundle outState) { base.OnSaveInstanceState(outState); mapView?.OnSaveInstanceState(outState); } public override void OnLowMemory() { base.OnLowMemory(); mapView?.OnLowMemory(); } #endregion } }