File: VersionTracking\VersionTracking.shared.cs
Web Access
Project: src\src\Essentials\src\Essentials.csproj (Microsoft.Maui.Essentials)
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Maui.Storage;
 
namespace Microsoft.Maui.ApplicationModel
{
	/// <summary>
	/// The VersionTracking API provides an easy way to track an app's version on a device.
	/// </summary>
	public interface IVersionTracking
	{
		/// <summary>
		/// Starts tracking version information.
		/// </summary>
		void Track();
 
		/// <summary>
		/// Gets a value indicating whether this is the first time this app has ever been launched on this device.
		/// </summary>
		bool IsFirstLaunchEver { get; }
 
		/// <summary>
		/// Gets a value indicating if this is the first launch of the app for the current version number.
		/// </summary>
		bool IsFirstLaunchForCurrentVersion { get; }
 
		/// <summary>
		/// Gets a value indicating if this is the first launch of the app for the current build number.
		/// </summary>
		bool IsFirstLaunchForCurrentBuild { get; }
 
		/// <summary>
		/// Gets the current version number of the app.
		/// </summary>
		string CurrentVersion { get; }
 
		/// <summary>
		/// Gets the current build of the app.
		/// </summary>
		string CurrentBuild { get; }
 
		/// <summary>
		/// Gets the version number for the previously run version.
		/// </summary>
		string? PreviousVersion { get; }
 
		/// <summary>
		/// Gets the build number for the previously run version.
		/// </summary>
		string? PreviousBuild { get; }
 
		/// <summary>
		/// Gets the version number of the first version of the app that was installed on this device.
		/// </summary>
		string? FirstInstalledVersion { get; }
 
		/// <summary>
		/// Gets the build number of first version of the app that was installed on this device.
		/// </summary>
		string? FirstInstalledBuild { get; }
 
		/// <summary>
		/// Gets the collection of version numbers of the app that ran on this device.
		/// </summary>
		IReadOnlyList<string> VersionHistory { get; }
 
		/// <summary>
		/// Gets the collection of build numbers of the app that ran on this device.
		/// </summary>
		IReadOnlyList<string> BuildHistory { get; }
 
		/// <summary>
		/// Determines if this is the first launch of the app for a specified version number.
		/// </summary>
		/// <param name="version">The version number.</param>
		/// <returns><see langword="true"/> if this is the first launch of the app for the specified version number; otherwise <see langword="false"/>.</returns>
		bool IsFirstLaunchForVersion(string version);
 
		/// <summary>
		/// Determines if this is the first launch of the app for a specified build number.
		/// </summary>
		/// <param name="build">The build number.</param>
		/// <returns><see langword="true"/> if this is the first launch of the app for the specified build number; otherwise <see langword="false"/>.</returns>
		bool IsFirstLaunchForBuild(string build);
	}
 
	/// <summary>
	/// The VersionTracking API provides an easy way to track an app's version on a device.
	/// </summary>
	public static partial class VersionTracking
	{
		/// <summary>
		/// Starts tracking version information.
		/// </summary>
		public static void Track()
			=> Default.Track();
 
		/// <summary>
		/// Gets a value indicating whether this is the first time this app has ever been launched on this device.
		/// </summary>
		public static bool IsFirstLaunchEver
			=> Default.IsFirstLaunchEver;
 
		/// <summary>
		/// Gets a value indicating if this is the first launch of the app for the current version number.
		/// </summary>
		public static bool IsFirstLaunchForCurrentVersion
			=> Default.IsFirstLaunchForCurrentVersion;
 
		/// <summary>
		/// Gets a value indicating if this is the first launch of the app for the current build number.
		/// </summary>
		public static bool IsFirstLaunchForCurrentBuild
			=> Default.IsFirstLaunchForCurrentBuild;
 
		/// <summary>
		/// Gets the current version number of the app.
		/// </summary>
		public static string CurrentVersion
			=> Default.CurrentVersion;
 
		/// <summary>
		/// Gets the current build of the app.
		/// </summary>
		public static string CurrentBuild
			=> Default.CurrentBuild;
 
		/// <summary>
		/// Gets the version number for the previously run version.
		/// </summary>
		public static string? PreviousVersion
			=> Default.PreviousVersion;
 
		/// <summary>
		/// Gets the build number for the previously run version.
		/// </summary>
		public static string? PreviousBuild
			=> Default.PreviousBuild;
 
		/// <summary>
		/// Gets the version number of the first version of the app that was installed on this device.
		/// </summary>
		public static string? FirstInstalledVersion
			=> Default.FirstInstalledVersion;
 
		/// <summary>
		/// Gets the build number of first version of the app that was installed on this device.
		/// </summary>
		public static string? FirstInstalledBuild
			=> Default.FirstInstalledBuild;
 
		/// <summary>
		/// Gets the collection of version numbers of the app that ran on this device.
		/// </summary>
		public static IEnumerable<string> VersionHistory
			=> Default.VersionHistory;
 
		/// <summary>
		/// Gets the collection of build numbers of the app that ran on this device.
		/// </summary>
		public static IEnumerable<string> BuildHistory
			=> Default.BuildHistory;
 
		/// <summary>
		/// Determines if this is the first launch of the app for a specified version number.
		/// </summary>
		/// <param name="version">The version number.</param>
		/// <returns><see langword="true"/> if this is the first launch of the app for the specified version number; otherwise <see langword="false"/>.</returns>
		public static bool IsFirstLaunchForVersion(string version)
			=> Default.IsFirstLaunchForVersion(version);
 
		/// <summary>
		/// Determines if this is the first launch of the app for a specified build number.
		/// </summary>
		/// <param name="build">The build number.</param>
		/// <returns><see langword="true"/> if this is the first launch of the app for the specified build number; otherwise <see langword="false"/>.</returns>
		public static bool IsFirstLaunchForBuild(string build)
			=> Default.IsFirstLaunchForBuild(build);
 
		static IVersionTracking? defaultImplementation;
 
		/// <summary>
		/// Provides the default implementation for static usage of this API.
		/// </summary>
		public static IVersionTracking Default =>
			defaultImplementation ??= new VersionTrackingImplementation(Preferences.Default, AppInfo.Current);
 
		internal static void SetDefault(IVersionTracking? implementation) =>
			defaultImplementation = implementation;
 
		internal static void InitVersionTracking() =>
			(Default as VersionTrackingImplementation)?.InitVersionTracking();
	}
 
	class VersionTrackingImplementation : IVersionTracking
	{
		const string versionsKey = "VersionTracking.Versions";
		const string buildsKey = "VersionTracking.Builds";
 
		static readonly string sharedName = Preferences.GetPrivatePreferencesSharedName("versiontracking");
 
		readonly IPreferences preferences;
		readonly IAppInfo appInfo;
 
		Dictionary<string, List<string>> versionTrail = null!;
 
		string LastInstalledVersion => versionTrail[versionsKey]?.LastOrDefault() ?? string.Empty;
 
		string LastInstalledBuild => versionTrail[buildsKey]?.LastOrDefault() ?? string.Empty;
 
		public VersionTrackingImplementation(IPreferences preferences, IAppInfo appInfo)
		{
			this.preferences = preferences;
			this.appInfo = appInfo;
 
			Track();
		}
 
		public void Track()
		{
			if (versionTrail != null)
				return;
 
			InitVersionTracking();
		}
 
		/// <summary>
		/// Initialize VersionTracking module, load data and track current version
		/// </summary>
		/// <remarks>
		/// For internal use. Usually only called once in production code, but multiple times in unit tests
		/// </remarks>
		internal void InitVersionTracking()
		{
			IsFirstLaunchEver = !preferences.ContainsKey(versionsKey, sharedName) || !preferences.ContainsKey(buildsKey, sharedName);
			if (IsFirstLaunchEver)
			{
				versionTrail = new(StringComparer.Ordinal)
				{
					{ versionsKey, new List<string>() },
					{ buildsKey, new List<string>() }
				};
			}
			else
			{
				versionTrail = new(StringComparer.Ordinal)
				{
					{ versionsKey, ReadHistory(versionsKey).ToList() },
					{ buildsKey, ReadHistory(buildsKey).ToList() }
				};
			}
 
			IsFirstLaunchForCurrentVersion = !versionTrail[versionsKey].Contains(CurrentVersion) || CurrentVersion != LastInstalledVersion;
			if (IsFirstLaunchForCurrentVersion)
			{
				// Avoid duplicates and move current version to end of list if already present
				versionTrail[versionsKey].RemoveAll(v => v == CurrentVersion);
				versionTrail[versionsKey].Add(CurrentVersion);
			}
 
			IsFirstLaunchForCurrentBuild = !versionTrail[buildsKey].Contains(CurrentBuild) || CurrentBuild != LastInstalledBuild;
			if (IsFirstLaunchForCurrentBuild)
			{
				// Avoid duplicates and move current build to end of list if already present
				versionTrail[buildsKey].RemoveAll(b => b == CurrentBuild);
				versionTrail[buildsKey].Add(CurrentBuild);
			}
 
			if (IsFirstLaunchForCurrentVersion || IsFirstLaunchForCurrentBuild)
			{
				WriteHistory(versionsKey, versionTrail[versionsKey]);
				WriteHistory(buildsKey, versionTrail[buildsKey]);
			}
		}
 
		public bool IsFirstLaunchEver { get; private set; }
 
		public bool IsFirstLaunchForCurrentVersion { get; private set; }
 
		public bool IsFirstLaunchForCurrentBuild { get; private set; }
 
		public string CurrentVersion => appInfo.VersionString;
 
		public string CurrentBuild => appInfo.BuildString;
 
		public string? PreviousVersion => GetPrevious(versionsKey);
 
		public string? PreviousBuild => GetPrevious(buildsKey);
 
		public string? FirstInstalledVersion => versionTrail[versionsKey].FirstOrDefault();
 
		public string? FirstInstalledBuild => versionTrail[buildsKey].FirstOrDefault();
 
		public IReadOnlyList<string> VersionHistory => versionTrail[versionsKey].ToArray();
 
		public IReadOnlyList<string> BuildHistory => versionTrail[buildsKey].ToArray();
 
		public bool IsFirstLaunchForVersion(string version)
			=> CurrentVersion == version && IsFirstLaunchForCurrentVersion;
 
		public bool IsFirstLaunchForBuild(string build)
			=> CurrentBuild == build && IsFirstLaunchForCurrentBuild;
 
		public string GetStatus()
		{
			var sb = new StringBuilder();
			sb.AppendLine();
			sb.AppendLine("VersionTracking");
			sb.AppendLine($"  IsFirstLaunchEver:              {IsFirstLaunchEver}");
			sb.AppendLine($"  IsFirstLaunchForCurrentVersion: {IsFirstLaunchForCurrentVersion}");
			sb.AppendLine($"  IsFirstLaunchForCurrentBuild:   {IsFirstLaunchForCurrentBuild}");
			sb.AppendLine();
			sb.AppendLine($"  CurrentVersion:                 {CurrentVersion}");
			sb.AppendLine($"  PreviousVersion:                {PreviousVersion}");
			sb.AppendLine($"  FirstInstalledVersion:          {FirstInstalledVersion}");
			sb.AppendLine($"  VersionHistory:                 [{string.Join(", ", VersionHistory)}]");
			sb.AppendLine();
			sb.AppendLine($"  CurrentBuild:                   {CurrentBuild}");
			sb.AppendLine($"  PreviousBuild:                  {PreviousBuild}");
			sb.AppendLine($"  FirstInstalledBuild:            {FirstInstalledBuild}");
			sb.AppendLine($"  BuildHistory:                   [{string.Join(", ", BuildHistory)}]");
			return sb.ToString();
		}
 
		string[] ReadHistory(string key)
			=> preferences.Get<string?>(key, null, sharedName)?.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty<string>();
 
		void WriteHistory(string key, IEnumerable<string> history)
			=> preferences.Set(key, string.Join("|", history), sharedName);
 
		string? GetPrevious(string key)
		{
			var trail = versionTrail[key];
			return (trail.Count >= 2) ? trail[trail.Count - 2] : null;
		}
	}
}