Code updated to 3.0.238

This commit is contained in:
Oliver Hauff 2021-06-26 20:57:55 +02:00
parent 3302d80678
commit 9c6a1fa92b
257 changed files with 7763 additions and 2861 deletions

View file

@ -4,7 +4,7 @@ using TINK.Model.Station;
using System;
using System.Linq;
using TINK.Model.Bike;
using TINK.Model.Repository.Exception;
using TINK.Repository.Exception;
using TINK.Model;
using Serilog;
using System.Collections.Generic;
@ -12,7 +12,9 @@ using System.Threading.Tasks;
using System.ComponentModel;
using Xamarin.Forms.GoogleMaps;
using System.Collections.ObjectModel;
#if USEMASTERDETAIL || USEFLYOUT
using TINK.View.MasterDetail;
#endif
using TINK.Settings;
using TINK.Model.Connector;
using TINK.Model.Services.CopriApi;
@ -24,6 +26,9 @@ using TINK.MultilingualResources;
using TINK.Services.BluetoothLock;
using TINK.Model.Services.CopriApi.ServerUris;
using TINK.ViewModel.Info;
using TINK.Repository;
using Plugin.Permissions.Abstractions;
using TINK.Model.Services.Geolocation;
#if !TRYNOTBACKSTYLE
#endif
@ -43,6 +48,18 @@ namespace TINK.ViewModel.Map
/// </summary>
private Exception m_oException;
/// <summary>
/// Service to query/ manage permissions (location) of the app.
/// </summary>
private IPermissions PermissionsService { get; }
/// <summary>
/// Service to manage bluetooth stack.
/// </summary>
private Plugin.BLE.Abstractions.Contracts.IBluetoothLE BluetoothService { get; set; }
/// <summary> Notifies view about changes. </summary>
public event PropertyChangedEventHandler PropertyChanged;
@ -58,9 +75,10 @@ namespace TINK.ViewModel.Map
/// <summary>Delegate to perform navigation.</summary>
private INavigation m_oNavigation;
#if USEMASTERDETAIL || USEFLYOUT
/// <summary>Delegate to perform navigation.</summary>
private INavigationMasterDetail m_oNavigationMasterDetail;
#endif
private ObservableCollection<Pin> pins;
public ObservableCollection<Pin> Pins
@ -81,7 +99,9 @@ namespace TINK.ViewModel.Map
/// <summary> False if user tabed on station marker to show bikes at a given station.</summary>
private bool isMapPageEnabled = false;
Model.Services.Geolocation.IGeolocation GeolocationService { get; }
/// <summary> False if user tabed on station marker to show bikes at a given station.</summary>
public bool IsMapPageEnabled {
get => isMapPageEnabled;
@ -97,30 +117,44 @@ namespace TINK.ViewModel.Map
/// <summary> Prevents an invalid instane to be created. </summary>
/// <param name="tinkApp"> Reference to tink app model.</param>
/// <param name="p_oMoveToRegionDelegate">Delegate to center map and set zoom level.</param>
/// <param name="p_oViewService">View service to notify user.</param>
/// <param name="p_oNavigation">Interface to navigate.</param>
/// <param name="moveToRegionDelegate">Delegate to center map and set zoom level.</param>
/// <param name="viewService">View service to notify user.</param>
/// <param name="navigation">Interface to navigate.</param>
public MapPageViewModel(
ITinkApp tinkApp,
Action<MapSpan> p_oMoveToRegionDelegate,
IViewService p_oViewService,
INavigation p_oNavigation)
IPermissions permissionsService,
Plugin.BLE.Abstractions.Contracts.IBluetoothLE bluetoothService,
IGeolocation geolocationService,
Action<MapSpan> moveToRegionDelegate,
IViewService viewService,
INavigation navigation)
{
TinkApp = tinkApp
?? throw new ArgumentException("Can not instantiate map page view model- object. No tink app object available.");
m_oMoveToRegionDelegate = p_oMoveToRegionDelegate
PermissionsService = permissionsService ??
throw new ArgumentException($"Can not instantiate {nameof(MapPageViewModel)}. Permissions service object must never be null.");
BluetoothService = bluetoothService ??
throw new ArgumentException($"Can not instantiate {nameof(MapPageViewModel)}. Bluetooth service object must never be null.");
GeolocationService = geolocationService ??
throw new ArgumentException($"Can not instantiate {nameof(MapPageViewModel)}. Geolocation service object must never be null.");
m_oMoveToRegionDelegate = moveToRegionDelegate
?? throw new ArgumentException("Can not instantiate map page view model- object. No move delegate available.");
m_oViewService = p_oViewService
m_oViewService = viewService
?? throw new ArgumentException("Can not instantiate map page view model- object. No view available.");
m_oNavigation = p_oNavigation
m_oNavigation = navigation
?? throw new ArgumentException("Can not instantiate map page view model- object. No navigation service available.");
m_oViewUpdateManager = new IdlePollingUpdateTaskManager();
#if USEMASTERDETAIL || USEFLYOUT
m_oNavigationMasterDetail = new EmptyNavigationMasterDetail();
#endif
Polling = PollingParameters.NoPolling;
@ -143,42 +177,46 @@ namespace TINK.ViewModel.Map
}
}
#if USEMASTERDETAIL || USEFLYOUT
/// <summary> Delegate to perform navigation.</summary>
public INavigationMasterDetail NavigationMasterDetail
{
set { m_oNavigationMasterDetail = value; }
}
#endif
public Command<PinClickedEventArgs> PinClickedCommand => new Command<PinClickedEventArgs>(
args =>
{
OnStationClicked(int.Parse(args.Pin.Tag.ToString()));
OnStationClicked(args.Pin.Tag.ToString());
args.Handled = true; // Prevents map to be centered to selected pin.
});
/// <summary>
/// One time setup: Sets pins into map and connects to events.
/// </summary>
private void InitializePins(StationDictionary p_oStations)
private void InitializePins(StationDictionary stations)
{
// Add pins to stations.
Log.ForContext<MapPageViewModel>().Debug($"Request to draw {p_oStations.Count} pins.");
foreach (var l_oStation in p_oStations)
Log.ForContext<MapPageViewModel>().Debug($"Request to draw {stations.Count} pins.");
foreach (var station in stations)
{
if (l_oStation.Position == null)
if (station.Position == null)
{
// There should be no reason for a position object to be null but this alreay occurred in past.
Log.ForContext<MapPageViewModel>().Error("Postion object of station {@l_oStation} is null.", l_oStation);
Log.ForContext<MapPageViewModel>().Error("Postion object of station {@l_oStation} is null.", station);
continue;
}
var l_oPin = new Pin
{
Position = new Xamarin.Forms.GoogleMaps.Position(l_oStation.Position.Latitude, l_oStation.Position.Longitude),
Label = l_oStation.Id > CUSTOM_ICONS_COUNT
? l_oStation.GetStationName()
Position = new Xamarin.Forms.GoogleMaps.Position(station.Position.Latitude, station.Position.Longitude),
Label = long.TryParse(station.Id, out long stationId) && stationId > CUSTOM_ICONS_COUNT
? station.GetStationName()
: string.Empty, // Stations with custom icons have already a id marker. No need for a label.
Tag = l_oStation.Id,
Tag = station.Id,
IsVisible = false, // Set to false to prevent showing default icons (flickering).
};
@ -187,41 +225,41 @@ namespace TINK.ViewModel.Map
}
/// <summary> Update all stations from TINK. </summary>
/// <param name="p_oStationsColorList">List of colors to apply.</param>
private void UpdatePinsColor(IList<Color> p_oStationsColorList)
/// <param name="stationsColorList">List of colors to apply.</param>
private void UpdatePinsColor(IList<Color> stationsColorList)
{
Log.ForContext<MapPageViewModel>().Debug($"Starting update of stations pins color for {p_oStationsColorList.Count} stations...");
Log.ForContext<MapPageViewModel>().Debug($"Starting update of stations pins color for {stationsColorList.Count} stations...");
// Update colors of pins.
for (int l_iPinIndex = 0; l_iPinIndex < p_oStationsColorList.Count; l_iPinIndex++)
for (int pinIndex = 0; pinIndex < stationsColorList.Count; pinIndex++)
{
var l_iStationId = int.Parse(Pins[l_iPinIndex].Tag.ToString());
var indexPartPrefix = l_iStationId <= CUSTOM_ICONS_COUNT
? $"{l_iStationId}" // there is a station marker with index letter for given station id
var indexPartPrefix = int.TryParse(Pins[pinIndex].Tag.ToString(), out int stationId)
&& stationId <= CUSTOM_ICONS_COUNT
? $"{stationId}" // there is a station marker with index letter for given station id
: "Open"; // there is no station marker. Use open marker.
var colorPartPrefix = GetRessourceNameColorPart(p_oStationsColorList[l_iPinIndex]);
var colorPartPrefix = GetRessourceNameColorPart(stationsColorList[pinIndex]);
var l_iName = $"{indexPartPrefix.ToString().PadLeft(2, '0')}_{colorPartPrefix}{(DeviceInfo.Platform == DevicePlatform.Android ? ".png" : string.Empty)}";
try
{
Pins[l_iPinIndex].Icon = BitmapDescriptorFactory.FromBundle(l_iName);
Pins[pinIndex].Icon = BitmapDescriptorFactory.FromBundle(l_iName);
}
catch (Exception l_oException)
{
Log.ForContext<MapPageViewModel>().Error("Station icon {l_strName} can not be loaded. {@l_oException}.", l_oException);
Pins[l_iPinIndex].Label = l_iStationId.ToString();
Pins[l_iPinIndex].Icon = BitmapDescriptorFactory.DefaultMarker(p_oStationsColorList[l_iPinIndex]);
Pins[pinIndex].Label = stationId.ToString();
Pins[pinIndex].Icon = BitmapDescriptorFactory.DefaultMarker(stationsColorList[pinIndex]);
}
Pins[l_iPinIndex].IsVisible = true;
Pins[pinIndex].IsVisible = true;
}
var pinsCount = Pins.Count;
for (int pinIndex = p_oStationsColorList.Count; pinIndex < pinsCount; pinIndex++)
for (int pinIndex = stationsColorList.Count; pinIndex < pinsCount; pinIndex++)
{
Log.ForContext<MapPageViewModel>().Error($"Unexpected count of pins detected. Expected {p_oStationsColorList.Count} but is {pinsCount}.");
Log.ForContext<MapPageViewModel>().Error($"Unexpected count of pins detected. Expected {stationsColorList.Count} but is {pinsCount}.");
Pins[pinIndex].IsVisible = false;
}
@ -229,31 +267,31 @@ namespace TINK.ViewModel.Map
}
/// <summary> Gets the color related part of the ressrouce name.</summary>
/// <param name="p_oColor">Color to get name for.</param>
/// <param name="color">Color to get name for.</param>
/// <returns>Resource name.</returns>
private static string GetRessourceNameColorPart(Color p_oColor)
private static string GetRessourceNameColorPart(Color color)
{
if (p_oColor == Color.Blue)
if (color == Color.Blue)
{
return "Blue";
}
if (p_oColor == Color.Green)
if (color == Color.Green)
{
return "Green";
}
if (p_oColor == Color.LightBlue)
if (color == Color.LightBlue)
{
return "LightBlue";
}
if (p_oColor == Color.Red)
if (color == Color.Red)
{
return "Red";
}
return p_oColor.ToString();
return color.ToString();
}
/// <summary>
@ -283,13 +321,12 @@ namespace TINK.ViewModel.Map
ActionText = AppResources.ActivityTextMyBikesLoadingBikes;
// Check location permission
var _permissions = TinkApp.Permissions;
var status = await _permissions.CheckPermissionStatusAsync<LocationPermission>();
var status = await PermissionsService.CheckPermissionStatusAsync<LocationPermission>();
if (TinkApp.CenterMapToCurrentLocation
&& !TinkApp.GeolocationServices.Active.IsSimulation
&& !GeolocationService.IsSimulation
&& status != Plugin.Permissions.Abstractions.PermissionStatus.Granted)
{
var permissionResult = await _permissions.RequestPermissionAsync<LocationPermission>();
var permissionResult = await PermissionsService.RequestPermissionAsync<LocationPermission>();
if (permissionResult != Plugin.Permissions.Abstractions.PermissionStatus.Granted)
{
@ -302,7 +339,7 @@ namespace TINK.ViewModel.Map
if (dialogResult)
{
// User decided to give access to locations permissions.
_permissions.OpenAppSettings();
PermissionsService.OpenAppSettings();
ActionText = "";
IsRunning = false;
IsMapPageEnabled = true;
@ -317,7 +354,7 @@ namespace TINK.ViewModel.Map
try
{
currentLocation = TinkApp.CenterMapToCurrentLocation
? await TinkApp.GeolocationServices.Active.GetAsync()
? await GeolocationService.GetAsync()
: null;
}
catch (Exception ex)
@ -332,6 +369,16 @@ namespace TINK.ViewModel.Map
IsConnected = TinkApp.GetIsConnected();
var resultStationsAndBikes = await TinkApp.GetConnector(IsConnected).Query.GetBikesAndStationsAsync();
TinkApp.Stations = resultStationsAndBikes.Response.StationsAll;
if (Pins.Count > 0 && Pins.Count != resultStationsAndBikes.Response.StationsAll.Count)
{
// Either
// - user logged in/ logged out which might lead to more/ less stations beeing available
// - new stations were added/ existing ones remove
Pins.Clear();
}
// Check if there are alreay any pins to the map
// i.e detecte first call of member OnAppearing after construction
if (Pins.Count <= 0)
@ -341,13 +388,23 @@ namespace TINK.ViewModel.Map
// Map was not yet initialized.
// Get stations from Copri
Log.ForContext<MapPageViewModel>().Verbose("No pins detected on page.");
if (resultStationsAndBikes.Response.StationsAll.CopriVersion >= new Version(4, 1))
if (resultStationsAndBikes.Response.StationsAll.CopriVersion < CopriCallsStatic.UnsupportedVersionLower)
{
await m_oViewService.DisplayAlert(
"Warnung",
string.Format(AppResources.MessageAppVersionIsOutdated, ContactPageViewModel.GetAppName(TinkApp.Uris.ActiveUri)),
"OK");
AppResources.MessageWaring,
string.Format(AppResources.MessageCopriVersionIsOutdated, ContactPageViewModel.GetAppName(TinkApp.Uris.ActiveUri)),
AppResources.MessageAnswerOk);
Log.ForContext<MapPageViewModel>().Error($"Outdated version of app detected. Version expected is {resultStationsAndBikes.Response.StationsAll.CopriVersion}.");
}
if (resultStationsAndBikes.Response.StationsAll.CopriVersion >= CopriCallsStatic.UnsupportedVersionUpper)
{
await m_oViewService.DisplayAlert(
AppResources.MessageWaring,
string.Format(AppResources.MessageAppVersionIsOutdated, ContactPageViewModel.GetAppName(TinkApp.Uris.ActiveUri)),
AppResources.MessageAnswerOk);
Log.ForContext<MapPageViewModel>().Error($"Outdated version of app detected. Version expected is {resultStationsAndBikes.Response.StationsAll.CopriVersion}.");
}
@ -360,7 +417,7 @@ namespace TINK.ViewModel.Map
if (resultStationsAndBikes.Exception?.GetType() == typeof(AuthcookieNotDefinedException))
{
Log.ForContext<MapPageViewModel>().Error("Map page is shown (probable for the first time after startup of app) and COPRI copri an auth cookie not defined error.{@l_oException}", resultStationsAndBikes.Exception);
Log.ForContext<MapPageViewModel>().Error("Map page is shown (probable for the first time after startup of app) and COPRI auth cookie is not defined. {@l_oException}", resultStationsAndBikes.Exception);
// COPRI reports an auth cookie error.
await m_oViewService.DisplayAlert(
@ -375,12 +432,12 @@ namespace TINK.ViewModel.Map
// Update pin colors.
Log.ForContext<MapPageViewModel>().Verbose("Starting update pins color...");
var l_oColors = GetStationColors(
var colors = GetStationColors(
Pins.Select(x => x.Tag.ToString()).ToList(),
resultStationsAndBikes.Response.Bikes);
// Update pins color form count of bikes located at station.
UpdatePinsColor(l_oColors);
UpdatePinsColor(colors);
m_oViewUpdateManager = CreateUpdateTask();
@ -476,7 +533,7 @@ namespace TINK.ViewModel.Map
TinkApp.PostAction(
unused =>
{
ActionText = "Aktualisiere...";
ActionText = AppResources.ActivityTextUpdating;
IsConnected = TinkApp.GetIsConnected();
},
null);
@ -546,7 +603,7 @@ namespace TINK.ViewModel.Map
/// <summary> User clicked on a bike. </summary>
/// <param name="selectedStationId">Id of station user clicked on.</param>
public async void OnStationClicked(int selectedStationId)
public async void OnStationClicked(string selectedStationId)
{
try
{
@ -555,7 +612,8 @@ namespace TINK.ViewModel.Map
// Lock action to prevent multiple instances of "BikeAtStation" being opened.
IsMapPageEnabled = false;
TinkApp.SelectedStation = selectedStationId;
TinkApp.SelectedStation = TinkApp.Stations.FirstOrDefault(x => x.Id == selectedStationId)
?? new Station(selectedStationId, new List<string>(), null); // Station might not be in list StationDictinaly because this list is not updatd in background task.
#if TRYNOTBACKSTYLE
m_oNavigation.ShowPage(
@ -585,13 +643,13 @@ namespace TINK.ViewModel.Map
/// <summary>
/// Gets the list of station color for all stations.
/// </summary>
/// <param name="p_oStationsId">Station id list to get color for.</param>
/// <param name="stationsId">Station id list to get color for.</param>
/// <returns></returns>
private static IList<Color> GetStationColors(
IEnumerable<string> p_oStationsId,
IEnumerable<string> stationsId,
BikeCollection bikesAll)
{
if (p_oStationsId == null)
if (stationsId == null)
{
Log.ForContext<MapPageViewModel>().Debug("No stations available to update color for.");
return new List<Color>();
@ -601,41 +659,33 @@ namespace TINK.ViewModel.Map
{
// If object is null an error occurred querrying bikes availalbe or bikes occpied which results in an unknown state.
Log.ForContext<MapPageViewModel>().Error("No bikes available to determine pins color.");
return new List<Color>(p_oStationsId.Select(x => Color.Blue));
return new List<Color>(stationsId.Select(x => Color.Blue));
}
// Get state for each station.
var l_oColors = new List<Color>();
foreach (var l_strStationId in p_oStationsId)
var colors = new List<Color>();
foreach (var stationId in stationsId)
{
if (int.TryParse(l_strStationId, out int l_iStationId) == false)
{
// Station id is not valid.
Log.ForContext<MapPageViewModel>().Error($"A station id {l_strStationId} is invalid (not integer).");
l_oColors.Add(Color.Blue);
continue;
}
// Get color of given station.
var l_oBikesAtStation = bikesAll.Where(x => x.CurrentStation == l_iStationId);
if (l_oBikesAtStation.FirstOrDefault(x => x.State.Value != Model.State.InUseStateEnum.Disposable) != null)
var bikesAtStation = bikesAll.Where(x => x.CurrentStation == stationId).ToList();
if (bikesAtStation.FirstOrDefault(x => x.State.Value != Model.State.InUseStateEnum.Disposable) != null)
{
// There is at least one requested or booked bike
l_oColors.Add(Color.LightBlue);
colors.Add(Color.LightBlue);
continue;
}
if (l_oBikesAtStation.ToList().Count > 0)
if (bikesAtStation.ToList().Count > 0)
{
// There is at least one bike available
l_oColors.Add(Color.Green);
colors.Add(Color.Green);
continue;
}
l_oColors.Add(Color.Red);
colors.Add(Color.Red);
}
return l_oColors;
return colors;
}
/// <summary>
@ -733,12 +783,14 @@ namespace TINK.ViewModel.Map
if (Exception != null)
{
// An error occurred getting data from copri.
return Exception.GetShortErrorInfoText();
return TinkApp.IsReportLevelVerbose
? Exception.GetShortErrorInfoText()
: AppResources.ActivityTextException;
}
if (!IsConnected)
{
return "Offline.";
return AppResources.ActivityTextConnectionStateOffline;
}
return ActionText ?? string.Empty;
@ -806,13 +858,12 @@ namespace TINK.ViewModel.Map
Pins.Clear();
// Check location permission
var _permissions = TinkApp.Permissions;
var status = await _permissions.CheckPermissionStatusAsync<LocationPermission>();
var status = await PermissionsService.CheckPermissionStatusAsync<LocationPermission>();
if (TinkApp.CenterMapToCurrentLocation
&& !TinkApp.GeolocationServices.Active.IsSimulation
&& !GeolocationService.IsSimulation
&& status != Plugin.Permissions.Abstractions.PermissionStatus.Granted)
{
var permissionResult = await _permissions.RequestPermissionAsync<LocationPermission>();
var permissionResult = await PermissionsService.RequestPermissionAsync<LocationPermission>();
if (permissionResult != Plugin.Permissions.Abstractions.PermissionStatus.Granted)
{
@ -825,7 +876,7 @@ namespace TINK.ViewModel.Map
if (dialogResult)
{
// User decided to give access to locations permissions.
_permissions.OpenAppSettings();
PermissionsService.OpenAppSettings();
IsMapPageEnabled = true;
ActionText = "";
return;
@ -835,7 +886,7 @@ namespace TINK.ViewModel.Map
// Do not use property .State to get bluetooth state due
// to issue https://hausource.visualstudio.com/TINK/_workitems/edit/116 /
// see https://github.com/xabre/xamarin-bluetooth-le/issues/112#issuecomment-380994887
if (await CrossBluetoothLE.Current.GetBluetoothState() != Plugin.BLE.Abstractions.Contracts.BluetoothState.On)
if (await BluetoothService.GetBluetoothState() != Plugin.BLE.Abstractions.Contracts.BluetoothState.On)
{
await m_oViewService.DisplayAlert(
AppResources.MessageTitleHint,
@ -852,7 +903,7 @@ namespace TINK.ViewModel.Map
try
{
currentLocation = TinkApp.CenterMapToCurrentLocation
? await TinkApp.GeolocationServices.Active.GetAsync()
? await GeolocationService.GetAsync()
: null;
}
catch (Exception ex)