From 6c23172a3480e653dd5cba2239cf478b58eb9255 Mon Sep 17 00:00:00 2001 From: Martin Zikmund Date: Mon, 15 May 2023 15:11:15 +0200 Subject: [PATCH 01/29] chore: Adjust the Identity of Samples app --- src/SamplesApp/SamplesApp.UWP/Package.appxmanifest | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/SamplesApp/SamplesApp.UWP/Package.appxmanifest b/src/SamplesApp/SamplesApp.UWP/Package.appxmanifest index a225fa1e10a4..d92478cad127 100644 --- a/src/SamplesApp/SamplesApp.UWP/Package.appxmanifest +++ b/src/SamplesApp/SamplesApp.UWP/Package.appxmanifest @@ -9,15 +9,15 @@ IgnorableNamespaces="uap mp rescap uap2"> SamplesApp - SamplesApp + Uno Platform Assets\StoreLogo.png From c84bb00a7ed3d6496838a025097388c8b761ed2c Mon Sep 17 00:00:00 2001 From: Martin Zikmund Date: Mon, 15 May 2023 15:11:35 +0200 Subject: [PATCH 02/29] feat: Support PackageId on Skia and WASM --- .../IPackageIdExtension.skia.cs | 15 -- src/Uno.UWP/ApplicationModel/Package.Other.cs | 139 +++++++++--------- src/Uno.UWP/ApplicationModel/Package.cs | 2 +- src/Uno.UWP/ApplicationModel/PackageId.cs | 4 +- .../ApplicationModel/PackageId.other.cs | 39 +++++ .../ApplicationModel/PackageId.skia.cs | 28 ---- 6 files changed, 115 insertions(+), 112 deletions(-) delete mode 100644 src/Uno.UWP/ApplicationModel/IPackageIdExtension.skia.cs create mode 100644 src/Uno.UWP/ApplicationModel/PackageId.other.cs delete mode 100644 src/Uno.UWP/ApplicationModel/PackageId.skia.cs diff --git a/src/Uno.UWP/ApplicationModel/IPackageIdExtension.skia.cs b/src/Uno.UWP/ApplicationModel/IPackageIdExtension.skia.cs deleted file mode 100644 index 017c70d92658..000000000000 --- a/src/Uno.UWP/ApplicationModel/IPackageIdExtension.skia.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Windows.ApplicationModel; - -namespace Uno.ApplicationModel -{ - internal interface IPackageIdExtension - { - string FamilyName { get; } - - string FullName { get; } - - string Name { get; } - - PackageVersion Version { get; } - } -} diff --git a/src/Uno.UWP/ApplicationModel/Package.Other.cs b/src/Uno.UWP/ApplicationModel/Package.Other.cs index e06f2f3510e4..fde3a638432a 100644 --- a/src/Uno.UWP/ApplicationModel/Package.Other.cs +++ b/src/Uno.UWP/ApplicationModel/Package.Other.cs @@ -1,103 +1,110 @@ -#nullable enable #if !(__IOS__ || __ANDROID__ || __MACOS__) +#nullable enable using System; -using System.Collections.Generic; using System.Reflection; -using System.Security.Cryptography; using System.Xml; -using Uno.Extensions; using Uno.Foundation.Logging; -using Uno.UI; -using Windows.ApplicationModel.Core; -using Windows.ApplicationModel.Email.DataProvider; -using Windows.Storage; -namespace Windows.ApplicationModel +namespace Windows.ApplicationModel; + +public partial class Package { - public partial class Package - { - private const string PackageManifestName = "Package.appxmanifest"; - private static Assembly? _entryAssembly; - private string _displayName = ""; - private string _logo = "ms-appx://logo"; - private bool? _manifestParsed; + private const string PackageManifestName = "Package.appxmanifest"; - private bool GetInnerIsDevelopmentMode() => false; + private static Assembly? _entryAssembly; + private string _displayName = ""; + private string _logo = "ms-appx://logo"; + private bool _manifestParsed; - private DateTimeOffset GetInstallDate() => DateTimeOffset.Now; + private bool GetInnerIsDevelopmentMode() => false; - private string GetInstalledPath() - { - if (_entryAssembly?.Location is { Length: > 0 } location) - { - return global::System.IO.Path.GetDirectoryName(location) ?? ""; - } - else if (AppContext.BaseDirectory is { Length: > 0 } baseDirectory) - { - return global::System.IO.Path.GetDirectoryName(baseDirectory) ?? ""; - } + private DateTimeOffset GetInstallDate() => DateTimeOffset.Now; - return Environment.CurrentDirectory; + private string GetInstalledPath() + { + if (_entryAssembly?.Location is { Length: > 0 } location) + { + return global::System.IO.Path.GetDirectoryName(location) ?? ""; } - - public string DisplayName + else if (AppContext.BaseDirectory is { Length: > 0 } baseDirectory) { - get - { - TryParsePackageManifest(); - return _displayName; - } + return global::System.IO.Path.GetDirectoryName(baseDirectory) ?? ""; } - public Uri? Logo => - TryParsePackageManifest() && !string.IsNullOrWhiteSpace(_logo) ? new Uri(_logo, UriKind.RelativeOrAbsolute) : default; + return Environment.CurrentDirectory; + } - internal static void SetEntryAssembly(Assembly entryAssembly) + public string DisplayName + { + get { - _entryAssembly = entryAssembly; + TryParsePackageManifest(); + return _displayName; } + } - private bool TryParsePackageManifest() + public Uri Logo + { + get { - if (_entryAssembly != null && - !_manifestParsed.HasValue) - { - var manifest = _entryAssembly.GetManifestResourceStream(PackageManifestName); + TryParsePackageManifest(); + return new Uri(_logo, UriKind.RelativeOrAbsolute); + } + } + + internal static void SetEntryAssembly(Assembly entryAssembly) => _entryAssembly = entryAssembly; + + private void TryParsePackageManifest() + { + if (_entryAssembly != null && !_manifestParsed) + { + var manifest = _entryAssembly.GetManifestResourceStream(PackageManifestName); - if (manifest != null) + if (manifest != null) + { + try { - try - { - var doc = new XmlDocument(); - doc.Load(manifest); + var doc = new XmlDocument(); + doc.Load(manifest); - var nsmgr = new XmlNamespaceManager(doc.NameTable); - nsmgr.AddNamespace("d", "http://schemas.microsoft.com/appx/manifest/foundation/windows10"); + var nsmgr = new XmlNamespaceManager(doc.NameTable); + nsmgr.AddNamespace("d", "http://schemas.microsoft.com/appx/manifest/foundation/windows10"); - _displayName = doc.SelectSingleNode("/d:Package/d:Properties/d:DisplayName", nsmgr)?.InnerText ?? ""; - _logo = doc.SelectSingleNode("/d:Package/d:Properties/d:Logo", nsmgr)?.InnerText ?? ""; - _manifestParsed = true; - } - catch (Exception ex) + _displayName = doc.SelectSingleNode("/d:Package/d:Properties/d:DisplayName", nsmgr)?.InnerText ?? ""; + _logo = doc.SelectSingleNode("/d:Package/d:Properties/d:Logo", nsmgr)?.InnerText ?? ""; + + var idNode = doc.SelectSingleNode("/d:Package/d:Identity", nsmgr); + + if (idNode is not null) { - _manifestParsed = false; - if (this.Log().IsEnabled(Uno.Foundation.Logging.LogLevel.Error)) + Id.Name = idNode.Attributes?.GetNamedItem("Name")?.Value ?? ""; + + var versionString = idNode.Attributes?.GetNamedItem("Version")?.Value ?? ""; + if (Version.TryParse(versionString, out var version)) { - this.Log().Error($"Failed to read manifest [{PackageManifestName}]", ex); + Id.Version = new PackageVersion(version); } + + Id.Publisher = idNode.Attributes?.GetNamedItem("Publisher")?.Value ?? ""; } + + _manifestParsed = true; } - else + catch (Exception ex) { - _manifestParsed = false; - if (this.Log().IsEnabled(Uno.Foundation.Logging.LogLevel.Debug)) + if (this.Log().IsEnabled(Uno.Foundation.Logging.LogLevel.Error)) { - this.Log().Debug($"Skipping manifest reading, unable to find [{PackageManifestName}]"); + this.Log().Error($"Failed to read manifest [{PackageManifestName}]", ex); } } } - - return _manifestParsed ?? false; + else + { + if (this.Log().IsEnabled(Uno.Foundation.Logging.LogLevel.Debug)) + { + this.Log().Debug($"Skipping manifest reading, unable to find [{PackageManifestName}]"); + } + } } } } diff --git a/src/Uno.UWP/ApplicationModel/Package.cs b/src/Uno.UWP/ApplicationModel/Package.cs index b73607976c20..3de73bd6e919 100644 --- a/src/Uno.UWP/ApplicationModel/Package.cs +++ b/src/Uno.UWP/ApplicationModel/Package.cs @@ -14,7 +14,7 @@ internal Package() public bool IsDevelopmentMode => GetInnerIsDevelopmentMode(); - public PackageId Id => new PackageId(); + public PackageId Id { get; } = new(); public DateTimeOffset InstallDate => GetInstallDate(); diff --git a/src/Uno.UWP/ApplicationModel/PackageId.cs b/src/Uno.UWP/ApplicationModel/PackageId.cs index 400872f7fa23..2ef3819e3da7 100644 --- a/src/Uno.UWP/ApplicationModel/PackageId.cs +++ b/src/Uno.UWP/ApplicationModel/PackageId.cs @@ -1,3 +1,4 @@ +#if (__IOS__ || __ANDROID__ || __MACOS__) #pragma warning disable 108 // new keyword hiding #pragma warning disable 114 // new keyword hiding using System.Reflection; @@ -15,7 +16,6 @@ public sealed partial class PackageId [Uno.NotImplemented] public ProcessorArchitecture Architecture => ProcessorArchitecture.Unknown; -#if !__ANDROID__ && !__IOS__ && !__MACOS__ && !__SKIA__ [global::Uno.NotImplemented("__WASM__")] public string FamilyName => "Unknown"; @@ -27,7 +27,6 @@ public sealed partial class PackageId [global::Uno.NotImplemented("__WASM__")] public PackageVersion Version => new PackageVersion(Assembly.GetExecutingAssembly().GetVersionNumber()); -#endif [Uno.NotImplemented] public string Publisher => "Unknown"; @@ -45,3 +44,4 @@ public sealed partial class PackageId public string ProductId => "Unknown"; } } +#endif diff --git a/src/Uno.UWP/ApplicationModel/PackageId.other.cs b/src/Uno.UWP/ApplicationModel/PackageId.other.cs new file mode 100644 index 000000000000..5d64e2dcbfc9 --- /dev/null +++ b/src/Uno.UWP/ApplicationModel/PackageId.other.cs @@ -0,0 +1,39 @@ +#if !(__IOS__ || __ANDROID__ || __MACOS__) +using ProcessorArchitecture = Windows.System.ProcessorArchitecture; + +namespace Windows.ApplicationModel; + +public sealed partial class PackageId +{ + internal PackageId() + { + } + + [Uno.NotImplemented] + public ProcessorArchitecture Architecture => ProcessorArchitecture.Unknown; + + [global::Uno.NotImplemented("__WASM__, __SKIA__")] + public string FamilyName => "Unknown"; + + [global::Uno.NotImplemented("__WASM__, __SKIA__")] + public string FullName => "Unknown"; + + public string Name { get; internal set; } = "Unknown"; + + public PackageVersion Version { get; internal set; } + + public string Publisher { get; internal set; } = "Unknown"; + + [Uno.NotImplemented] + public string PublisherId => "Unknown"; + + [Uno.NotImplemented] + public string ResourceId => "Unknown"; + + [Uno.NotImplemented] + public string Author => "Unknown"; + + [Uno.NotImplemented] + public string ProductId => "Unknown"; +} +#endif diff --git a/src/Uno.UWP/ApplicationModel/PackageId.skia.cs b/src/Uno.UWP/ApplicationModel/PackageId.skia.cs deleted file mode 100644 index f593ba09eda4..000000000000 --- a/src/Uno.UWP/ApplicationModel/PackageId.skia.cs +++ /dev/null @@ -1,28 +0,0 @@ -#nullable enable - -using System.Reflection; -using Uno.ApplicationModel; -using Uno.Extensions; -using Uno.Foundation.Extensibility; - -namespace Windows.ApplicationModel -{ - public partial class PackageId - { - private IPackageIdExtension? _packageIdExtension; - - partial void InitializePlatform() - { - ApiExtensibility.CreateInstance(typeof(PackageId), out _packageIdExtension); - } - - public string FamilyName => _packageIdExtension?.FamilyName ?? "Unknown"; - - public string FullName => _packageIdExtension?.FullName ?? "Unknown"; - - public string Name => _packageIdExtension?.Name ?? "Unknown"; - - public PackageVersion Version => _packageIdExtension?.Version ?? - new PackageVersion(Assembly.GetExecutingAssembly().GetVersionNumber()); - } -} From 3e6840a52cc8092600a8233d0aefb2792660dbaa Mon Sep 17 00:00:00 2001 From: Martin Zikmund Date: Tue, 16 May 2023 18:24:03 +0200 Subject: [PATCH 03/29] feat: App-specific ApplicationData folders and settings --- src/Uno.UWP/ApplicationModel/Package.Other.cs | 101 +++++++++-------- src/Uno.UWP/ApplicationModel/Package.cs | 6 +- .../ApplicationModel/PackageId.other.cs | 4 +- src/Uno.UWP/Storage/ApplicationData.cs | 22 ++-- src/Uno.UWP/Storage/ApplicationData.skia.cs | 104 +++++++++++++++--- .../Storage/ApplicationDataContainer.cs | 28 ++--- .../Storage/ApplicationDataContainer.skia.cs | 32 +----- 7 files changed, 175 insertions(+), 122 deletions(-) diff --git a/src/Uno.UWP/ApplicationModel/Package.Other.cs b/src/Uno.UWP/ApplicationModel/Package.Other.cs index fde3a638432a..b6991b127990 100644 --- a/src/Uno.UWP/ApplicationModel/Package.Other.cs +++ b/src/Uno.UWP/ApplicationModel/Package.Other.cs @@ -12,9 +12,12 @@ public partial class Package private const string PackageManifestName = "Package.appxmanifest"; private static Assembly? _entryAssembly; - private string _displayName = ""; - private string _logo = "ms-appx://logo"; - private bool _manifestParsed; + + partial void InitializePlatform() + { + } + + internal static bool IsManifestInitialized { get; private set; } private bool GetInnerIsDevelopmentMode() => false; @@ -34,78 +37,74 @@ private string GetInstalledPath() return Environment.CurrentDirectory; } - public string DisplayName + public string DisplayName { get; private set; } = ""; + + public Uri Logo { get; private set; } = new Uri("ms-appx://logo", UriKind.RelativeOrAbsolute); + + internal static void SetEntryAssembly(Assembly entryAssembly) { - get - { - TryParsePackageManifest(); - return _displayName; - } + _entryAssembly = entryAssembly; + Current.Id.Name = entryAssembly.GetName().Name; // Set the package name to the entry assembly name by default. + Current.ParsePackageManifest(); + IsManifestInitialized = true; } - public Uri Logo + private void ParsePackageManifest() { - get + if (_entryAssembly is null) { - TryParsePackageManifest(); - return new Uri(_logo, UriKind.RelativeOrAbsolute); + return; } - } - internal static void SetEntryAssembly(Assembly entryAssembly) => _entryAssembly = entryAssembly; + var manifest = _entryAssembly.GetManifestResourceStream(PackageManifestName); - private void TryParsePackageManifest() - { - if (_entryAssembly != null && !_manifestParsed) + if (manifest is not null) { - var manifest = _entryAssembly.GetManifestResourceStream(PackageManifestName); - - if (manifest != null) + try { - try - { - var doc = new XmlDocument(); - doc.Load(manifest); - - var nsmgr = new XmlNamespaceManager(doc.NameTable); - nsmgr.AddNamespace("d", "http://schemas.microsoft.com/appx/manifest/foundation/windows10"); + var doc = new XmlDocument(); + doc.Load(manifest); - _displayName = doc.SelectSingleNode("/d:Package/d:Properties/d:DisplayName", nsmgr)?.InnerText ?? ""; - _logo = doc.SelectSingleNode("/d:Package/d:Properties/d:Logo", nsmgr)?.InnerText ?? ""; + var nsmgr = new XmlNamespaceManager(doc.NameTable); + nsmgr.AddNamespace("d", "http://schemas.microsoft.com/appx/manifest/foundation/windows10"); - var idNode = doc.SelectSingleNode("/d:Package/d:Identity", nsmgr); + DisplayName = doc.SelectSingleNode("/d:Package/d:Properties/d:DisplayName", nsmgr)?.InnerText ?? ""; - if (idNode is not null) - { - Id.Name = idNode.Attributes?.GetNamedItem("Name")?.Value ?? ""; - - var versionString = idNode.Attributes?.GetNamedItem("Version")?.Value ?? ""; - if (Version.TryParse(versionString, out var version)) - { - Id.Version = new PackageVersion(version); - } - - Id.Publisher = idNode.Attributes?.GetNamedItem("Publisher")?.Value ?? ""; - } - - _manifestParsed = true; + var logoUri = doc.SelectSingleNode("/d:Package/d:Properties/d:Logo", nsmgr)?.InnerText ?? ""; + if (Uri.TryCreate(logoUri, UriKind.RelativeOrAbsolute, out var logo)) + { + Logo = logo; } - catch (Exception ex) + + var idNode = doc.SelectSingleNode("/d:Package/d:Identity", nsmgr); + if (idNode is not null) { - if (this.Log().IsEnabled(Uno.Foundation.Logging.LogLevel.Error)) + Id.Name = idNode.Attributes?.GetNamedItem("Name")?.Value ?? ""; + + var versionString = idNode.Attributes?.GetNamedItem("Version")?.Value ?? ""; + if (Version.TryParse(versionString, out var version)) { - this.Log().Error($"Failed to read manifest [{PackageManifestName}]", ex); + Id.Version = new PackageVersion(version); } + + Id.Publisher = idNode.Attributes?.GetNamedItem("Publisher")?.Value ?? ""; } } - else + catch (Exception ex) { - if (this.Log().IsEnabled(Uno.Foundation.Logging.LogLevel.Debug)) + if (this.Log().IsEnabled(Uno.Foundation.Logging.LogLevel.Error)) { - this.Log().Debug($"Skipping manifest reading, unable to find [{PackageManifestName}]"); + this.Log().Error($"Failed to read manifest [{PackageManifestName}]", ex); } } } + else + { + if (this.Log().IsEnabled(Uno.Foundation.Logging.LogLevel.Debug)) + { + this.Log().Debug($"Skipping manifest reading, unable to find [{PackageManifestName}]"); + } + } } } #endif diff --git a/src/Uno.UWP/ApplicationModel/Package.cs b/src/Uno.UWP/ApplicationModel/Package.cs index 3de73bd6e919..966cc124207f 100644 --- a/src/Uno.UWP/ApplicationModel/Package.cs +++ b/src/Uno.UWP/ApplicationModel/Package.cs @@ -8,9 +8,9 @@ public partial class Package { private StorageFolder _installedLocation; - internal Package() - { - } + internal Package() => InitializePlatform(); + + partial void InitializePlatform(); public bool IsDevelopmentMode => GetInnerIsDevelopmentMode(); diff --git a/src/Uno.UWP/ApplicationModel/PackageId.other.cs b/src/Uno.UWP/ApplicationModel/PackageId.other.cs index 5d64e2dcbfc9..7c9c365d6237 100644 --- a/src/Uno.UWP/ApplicationModel/PackageId.other.cs +++ b/src/Uno.UWP/ApplicationModel/PackageId.other.cs @@ -18,11 +18,11 @@ internal PackageId() [global::Uno.NotImplemented("__WASM__, __SKIA__")] public string FullName => "Unknown"; - public string Name { get; internal set; } = "Unknown"; + public string Name { get; internal set; } = ""; public PackageVersion Version { get; internal set; } - public string Publisher { get; internal set; } = "Unknown"; + public string Publisher { get; internal set; } = ""; [Uno.NotImplemented] public string PublisherId => "Unknown"; diff --git a/src/Uno.UWP/Storage/ApplicationData.cs b/src/Uno.UWP/Storage/ApplicationData.cs index 0529360f6c49..ed015376d5a7 100644 --- a/src/Uno.UWP/Storage/ApplicationData.cs +++ b/src/Uno.UWP/Storage/ApplicationData.cs @@ -12,33 +12,37 @@ public sealed partial class ApplicationData private ApplicationData() { + PartialCtor(); + + LocalFolder = new StorageFolder(GetLocalFolder()); + RoamingFolder = new StorageFolder(GetRoamingFolder()); + SharedLocalFolder = new StorageFolder(".shared", GetSharedLocalFolder()); + LocalCacheFolder = new StorageFolder(GetLocalCacheFolder()); + TemporaryFolder = new StorageFolder(GetTemporaryFolder()); + LocalSettings = new ApplicationDataContainer(this, "Local", ApplicationDataLocality.Local); RoamingSettings = new ApplicationDataContainer(this, "Roaming", ApplicationDataLocality.Roaming); - - PartialCtor(); } partial void PartialCtor(); - public StorageFolder LocalFolder { get; } = new StorageFolder(GetLocalFolder()); + public StorageFolder LocalFolder { get; } - public StorageFolder RoamingFolder { get; } = new StorageFolder(GetRoamingFolder()); + public StorageFolder RoamingFolder { get; } - public StorageFolder SharedLocalFolder { get; } = new StorageFolder(".shared", GetSharedLocalFolder()); + public StorageFolder SharedLocalFolder { get; } - public StorageFolder LocalCacheFolder { get; } = new StorageFolder(GetLocalCacheFolder()); + public StorageFolder LocalCacheFolder { get; } - public StorageFolder TemporaryFolder { get; } = new StorageFolder(GetTemporaryFolder()); + public StorageFolder TemporaryFolder { get; } public ApplicationDataContainer LocalSettings { get; } public ApplicationDataContainer RoamingSettings { get; } - [Uno.NotImplemented] public ulong RoamingStorageQuota => 0; - [Uno.NotImplemented] public uint Version => 0; diff --git a/src/Uno.UWP/Storage/ApplicationData.skia.cs b/src/Uno.UWP/Storage/ApplicationData.skia.cs index b5104c9c5475..77873076f2cf 100644 --- a/src/Uno.UWP/Storage/ApplicationData.skia.cs +++ b/src/Uno.UWP/Storage/ApplicationData.skia.cs @@ -1,24 +1,102 @@ +#nullable enable + using System; +using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Linq; +using Windows.ApplicationModel; + +namespace Windows.Storage; -namespace Windows.Storage +partial class ApplicationData { - partial class ApplicationData + private const string DistinguishedNameOrganizationPrefix = "O="; + private const string DistinguishedNameCommonNamePrefix = "CN="; + private const string LocalCacheFolderName = "LocalCache"; + private const string TemporaryFolderName = "TempState"; + private const string LocalFolderName = "LocalState"; + private const string SharedLocalFolderName = "SharedLocalState"; + private const string RoamingFolderName = "RoamingState"; + private const string SettingsFolderName = "Settings"; + + private static string _appSpecificSubpath = null!; + + partial void PartialCtor() => EnsureAppSubpath(); + + private string GetLocalCacheFolder() => EnsurePath(Path.Combine(Path.GetTempPath(), _appSpecificSubpath, LocalCacheFolderName)); + + private string GetTemporaryFolder() => EnsurePath(Path.Combine(Path.GetTempPath(), _appSpecificSubpath, TemporaryFolderName)); + + private string GetLocalFolder() => + // Uses XDG_DATA_HOME on Unix: https://github.com/dotnet/runtime/blob/b5705587347d29d79cec830dc22b389e1ad9a9e0/src/libraries/System.Private.CoreLib/src/System/Environment.GetFolderPathCore.Unix.cs#L105 + EnsurePath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), _appSpecificSubpath, LocalFolderName)); + + private string GetRoamingFolder() => + EnsurePath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), _appSpecificSubpath, RoamingFolderName)); + + private string GetSharedLocalFolder() => + EnsurePath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), _appSpecificSubpath, SharedLocalFolderName)); + + internal string GetSettingsFolderPath() => + EnsurePath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), _appSpecificSubpath, SettingsFolderName)); + + private string EnsurePath(string path) { - private static string GetLocalCacheFolder() - => Path.GetTempPath(); + Directory.CreateDirectory(path); + return path; + } + + [MemberNotNull(nameof(_appSpecificSubpath))] + private static void EnsureAppSubpath() + { + if (_appSpecificSubpath is not null) + { + return; + } + + if (!Package.IsManifestInitialized) + { + throw new InvalidOperationException("The Package.Id is not initialized yet."); + } - private static string GetTemporaryFolder() - => Path.GetTempPath(); + var appName = Package.Current.Id.Name; + var appNameSafe = GetFileNameSafeString(appName); - private static string GetLocalFolder() - // Uses XDG_DATA_HOME on Unix: https://github.com/dotnet/runtime/blob/b5705587347d29d79cec830dc22b389e1ad9a9e0/src/libraries/System.Private.CoreLib/src/System/Environment.GetFolderPathCore.Unix.cs#L105 - => Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + var publisherDistinguishedName = Package.Current.Id.Publisher; + string? publisherName = null; + if (!string.IsNullOrEmpty(publisherDistinguishedName)) + { + var parts = publisherDistinguishedName.Split(','); + if (parts.FirstOrDefault(p => p.StartsWith(DistinguishedNameOrganizationPrefix, StringComparison.OrdinalIgnoreCase)) is { } organizationPart) + { + publisherName = organizationPart.Substring(DistinguishedNameOrganizationPrefix.Length); + } - private static string GetRoamingFolder() - => Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + if (string.IsNullOrEmpty(publisherName) && + parts.FirstOrDefault(p => p.StartsWith(DistinguishedNameCommonNamePrefix, StringComparison.OrdinalIgnoreCase)) is { } commonNamePart) + { + publisherName = commonNamePart.Substring(DistinguishedNameCommonNamePrefix.Length); + } + } - private static string GetSharedLocalFolder() - => Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + var publisherNameSafe = !string.IsNullOrEmpty(publisherName) ? GetFileNameSafeString(publisherName) : null; + + if (publisherNameSafe is not null) + { + _appSpecificSubpath = Path.Combine(publisherNameSafe, appNameSafe); + } + else + { + _appSpecificSubpath = appNameSafe; + } + } + + private static string GetFileNameSafeString(string fileName) + { + foreach (char c in Path.GetInvalidFileNameChars()) + { + fileName = fileName.Replace(c, '_'); + } + return fileName; } } diff --git a/src/Uno.UWP/Storage/ApplicationDataContainer.cs b/src/Uno.UWP/Storage/ApplicationDataContainer.cs index 46a5c7f3a99c..62a56131f1b1 100644 --- a/src/Uno.UWP/Storage/ApplicationDataContainer.cs +++ b/src/Uno.UWP/Storage/ApplicationDataContainer.cs @@ -1,26 +1,22 @@ -using System.Collections; -using System.Collections.Generic; -using System.Linq; using Windows.Foundation.Collections; -namespace Windows.Storage +namespace Windows.Storage; + +public partial class ApplicationDataContainer { - public partial class ApplicationDataContainer + internal ApplicationDataContainer(ApplicationData owner, string name, ApplicationDataLocality locality) { - internal ApplicationDataContainer(ApplicationData owner, string name, ApplicationDataLocality locality) - { - Locality = locality; - Name = name; + Locality = locality; + Name = name; - InitializePartial(owner); - } + InitializePartial(owner); + } - partial void InitializePartial(ApplicationData owner); + partial void InitializePartial(ApplicationData owner); - public ApplicationDataLocality Locality { get; } + public ApplicationDataLocality Locality { get; } - public string Name { get; } + public string Name { get; } - public IPropertySet Values { get; private set; } - } + public IPropertySet Values { get; private set; } } diff --git a/src/Uno.UWP/Storage/ApplicationDataContainer.skia.cs b/src/Uno.UWP/Storage/ApplicationDataContainer.skia.cs index 7ac6e3389342..064e0c635d81 100644 --- a/src/Uno.UWP/Storage/ApplicationDataContainer.skia.cs +++ b/src/Uno.UWP/Storage/ApplicationDataContainer.skia.cs @@ -2,11 +2,8 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Linq; - -using Uno.Extensions; using Uno.Foundation.Logging; using Windows.Foundation.Collections; @@ -21,37 +18,16 @@ partial void InitializePartial(ApplicationData owner) private class FilePropertySet : IPropertySet { - private const string UWPFileName = ".UWPAppSettings"; - private readonly Dictionary _values = new Dictionary(); + private readonly Dictionary _values = new(); private readonly string _folderPath; private readonly string _filePath; public FilePropertySet(ApplicationData owner, ApplicationDataLocality locality) { - StorageFolder folder; - switch (locality) - { - case ApplicationDataLocality.Local: - folder = owner.LocalFolder; - break; - - case ApplicationDataLocality.Roaming: - folder = owner.RoamingFolder; - break; - case ApplicationDataLocality.LocalCache: - folder = owner.LocalCacheFolder; - break; - - case ApplicationDataLocality.Temporary: - folder = owner.TemporaryFolder; - break; - - default: - throw new ArgumentOutOfRangeException(nameof(locality)); - } + var settingsFolderPath = owner.GetSettingsFolderPath(); - _folderPath = folder.Path; - _filePath = Path.Combine(folder.Path, UWPFileName); + _folderPath = settingsFolderPath; + _filePath = Path.Combine(settingsFolderPath, $"{locality}.dat"); ReadFromFile(); } From 7b876d5138e4daa8699d155ba51556a8dcfdd08c Mon Sep 17 00:00:00 2001 From: Martin Zikmund Date: Tue, 20 Jun 2023 09:48:19 +0200 Subject: [PATCH 04/29] chore: Move ApplicationLanguages initialization --- src/Uno.UI.Runtime.Skia.Gtk/GtkHost.cs | 2 +- src/Uno.UI/UI/Xaml/Application.Android.cs | 2 ++ src/Uno.UI/UI/Xaml/Application.cs | 1 - src/Uno.UI/UI/Xaml/Application.iOS.cs | 2 ++ src/Uno.UI/UI/Xaml/Application.macOS.cs | 2 ++ src/Uno.UI/UI/Xaml/Application.reference.cs | 2 ++ src/Uno.UI/UI/Xaml/Application.skia.cs | 5 +++-- src/Uno.UI/UI/Xaml/Application.unittests.cs | 2 ++ src/Uno.UI/UI/Xaml/Application.wasm.cs | 2 ++ 9 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/Uno.UI.Runtime.Skia.Gtk/GtkHost.cs b/src/Uno.UI.Runtime.Skia.Gtk/GtkHost.cs index b93e61609fd0..92f33e56ddb5 100644 --- a/src/Uno.UI.Runtime.Skia.Gtk/GtkHost.cs +++ b/src/Uno.UI.Runtime.Skia.Gtk/GtkHost.cs @@ -20,7 +20,7 @@ public partial class GtkHost : ISkiaApplicationHost { private const int UnoThemePriority = 800; - private readonly Func _appBuilder; + private readonly Func _appBuilder; [ThreadStatic] private static bool _isDispatcherThread; [ThreadStatic] private static GtkHost? _current; diff --git a/src/Uno.UI/UI/Xaml/Application.Android.cs b/src/Uno.UI/UI/Xaml/Application.Android.cs index 290439c6fa20..ded7ce01509c 100644 --- a/src/Uno.UI/UI/Xaml/Application.Android.cs +++ b/src/Uno.UI/UI/Xaml/Application.Android.cs @@ -6,6 +6,7 @@ using Windows.ApplicationModel.Activation; using Windows.Foundation; using Windows.Foundation.Metadata; +using Windows.Globalization; using Windows.UI.Xaml.Controls.Primitives; #if HAS_UNO_WINUI @@ -22,6 +23,7 @@ public Application() { Window.Current.ToString(); Current = this; + ApplicationLanguages.ApplyCulture(); InitializeSystemTheme(); PermissionsHelper.Initialize(); } diff --git a/src/Uno.UI/UI/Xaml/Application.cs b/src/Uno.UI/UI/Xaml/Application.cs index 1fc5c97c27a8..ae77e47cd159 100644 --- a/src/Uno.UI/UI/Xaml/Application.cs +++ b/src/Uno.UI/UI/Xaml/Application.cs @@ -231,7 +231,6 @@ public void Exit() public static void Start(global::Windows.UI.Xaml.ApplicationInitializationCallback callback) { - ApplicationLanguages.ApplyCulture(); StartPartial(callback); } diff --git a/src/Uno.UI/UI/Xaml/Application.iOS.cs b/src/Uno.UI/UI/Xaml/Application.iOS.cs index 9be4f265b4b6..1a28c36c026e 100644 --- a/src/Uno.UI/UI/Xaml/Application.iOS.cs +++ b/src/Uno.UI/UI/Xaml/Application.iOS.cs @@ -5,6 +5,7 @@ using Windows.ApplicationModel.Activation; using Windows.ApplicationModel; using ObjCRuntime; +using Windows.Globalization; using Windows.Graphics.Display; using Uno.Extensions; using Windows.UI.Core; @@ -31,6 +32,7 @@ public partial class Application : UIApplicationDelegate public Application() { Current = this; + ApplicationLanguages.ApplyCulture(); SetCurrentLanguage(); InitializeSystemTheme(); diff --git a/src/Uno.UI/UI/Xaml/Application.macOS.cs b/src/Uno.UI/UI/Xaml/Application.macOS.cs index ffe61720670f..f2a950aeadc7 100644 --- a/src/Uno.UI/UI/Xaml/Application.macOS.cs +++ b/src/Uno.UI/UI/Xaml/Application.macOS.cs @@ -3,6 +3,7 @@ using AppKit; using Windows.ApplicationModel.Activation; using Windows.ApplicationModel; +using Windows.Globalization; using System.Globalization; using Uno.Foundation.Logging; using System.Linq; @@ -35,6 +36,7 @@ static partial void InitializePartialStatic() public Application() { Current = this; + ApplicationLanguages.ApplyCulture(); SetCurrentLanguage(); InitializeSystemTheme(); diff --git a/src/Uno.UI/UI/Xaml/Application.reference.cs b/src/Uno.UI/UI/Xaml/Application.reference.cs index ec920198872a..72fb498eaa3c 100644 --- a/src/Uno.UI/UI/Xaml/Application.reference.cs +++ b/src/Uno.UI/UI/Xaml/Application.reference.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text; +using Windows.Globalization; namespace Windows.UI.Xaml; @@ -9,6 +10,7 @@ public partial class Application public Application() { Current = this; + ApplicationLanguages.ApplyCulture(); InitializeSystemTheme(); } } diff --git a/src/Uno.UI/UI/Xaml/Application.skia.cs b/src/Uno.UI/UI/Xaml/Application.skia.cs index d33f41555236..406057cb7b86 100644 --- a/src/Uno.UI/UI/Xaml/Application.skia.cs +++ b/src/Uno.UI/UI/Xaml/Application.skia.cs @@ -12,6 +12,7 @@ using System.Threading; using System.Globalization; using Windows.ApplicationModel.Core; +using Windows.Globalization; using Uno.UI.Xaml.Core; namespace Windows.UI.Xaml @@ -24,11 +25,11 @@ public partial class Application : IApplicationEvents public Application() { Current = this; + Package.SetEntryAssembly(this.GetType().Assembly); + ApplicationLanguages.ApplyCulture(); SetCurrentLanguage(); InitializeSystemTheme(); - Package.SetEntryAssembly(this.GetType().Assembly); - if (!_startInvoked) { throw new InvalidOperationException("The application must be started using Application.Start first, e.g. Windows.UI.Xaml.Application.Start(_ => new App());"); diff --git a/src/Uno.UI/UI/Xaml/Application.unittests.cs b/src/Uno.UI/UI/Xaml/Application.unittests.cs index ec920198872a..72fb498eaa3c 100644 --- a/src/Uno.UI/UI/Xaml/Application.unittests.cs +++ b/src/Uno.UI/UI/Xaml/Application.unittests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text; +using Windows.Globalization; namespace Windows.UI.Xaml; @@ -9,6 +10,7 @@ public partial class Application public Application() { Current = this; + ApplicationLanguages.ApplyCulture(); InitializeSystemTheme(); } } diff --git a/src/Uno.UI/UI/Xaml/Application.wasm.cs b/src/Uno.UI/UI/Xaml/Application.wasm.cs index 2b5c589b7860..bf84a04efd44 100644 --- a/src/Uno.UI/UI/Xaml/Application.wasm.cs +++ b/src/Uno.UI/UI/Xaml/Application.wasm.cs @@ -7,6 +7,7 @@ using Windows.Foundation.Metadata; using Windows.UI.Xaml.Controls.Primitives; using Windows.ApplicationModel; +using Windows.Globalization; using Windows.Graphics.Display; using Windows.UI.Core; using Uno.Foundation; @@ -46,6 +47,7 @@ public Application() Current = this; InitializeSystemTheme(); Package.SetEntryAssembly(this.GetType().Assembly); + ApplicationLanguages.ApplyCulture(); global::Uno.Foundation.Extensibility.ApiExtensibility.Register( typeof(global::Windows.ApplicationModel.DataTransfer.DragDrop.Core.IDragDropExtension), From da0a2830d109518e04aaa0616f65448cc7a468b8 Mon Sep 17 00:00:00 2001 From: Martin Zikmund Date: Tue, 20 Jun 2023 09:55:46 +0200 Subject: [PATCH 05/29] chore: Share Application initialization --- src/Uno.UI/UI/Xaml/Application.cs | 20 ++++++++++++++++++++ src/Uno.UI/UI/Xaml/Application.iOS.cs | 5 +---- src/Uno.UI/UI/Xaml/Application.macOS.cs | 5 +---- src/Uno.UI/UI/Xaml/Application.reference.cs | 16 ---------------- src/Uno.UI/UI/Xaml/Application.skia.cs | 6 +----- src/Uno.UI/UI/Xaml/Application.unittests.cs | 16 ---------------- src/Uno.UI/UI/Xaml/Application.wasm.cs | 7 +------ 7 files changed, 24 insertions(+), 51 deletions(-) delete mode 100644 src/Uno.UI/UI/Xaml/Application.reference.cs delete mode 100644 src/Uno.UI/UI/Xaml/Application.unittests.cs diff --git a/src/Uno.UI/UI/Xaml/Application.cs b/src/Uno.UI/UI/Xaml/Application.cs index ae77e47cd159..9ce190abe712 100644 --- a/src/Uno.UI/UI/Xaml/Application.cs +++ b/src/Uno.UI/UI/Xaml/Application.cs @@ -48,6 +48,9 @@ namespace Windows.UI.Xaml { + /// + /// Encapsulates the app and its available services. + /// public partial class Application { private bool _initializationComplete; @@ -77,6 +80,23 @@ static Application() InitializePartialStatic(); } + /// + /// Initializes a new instance of the Application class. + /// + public Application() + { +#if __SKIA__ || __WASM__ + Package.SetEntryAssembly(this.GetType().Assembly); +#endif + Current = this; + ApplicationLanguages.ApplyCulture(); + InitializeSystemTheme(); + + InitializePartial(); + } + + partial void InitializePartial(); + private static void RegisterExtensions() { ApiExtensibility.Register(typeof(IMessageDialogExtension), dialog => new MessageDialogExtension(dialog)); diff --git a/src/Uno.UI/UI/Xaml/Application.iOS.cs b/src/Uno.UI/UI/Xaml/Application.iOS.cs index 1a28c36c026e..72390b40c609 100644 --- a/src/Uno.UI/UI/Xaml/Application.iOS.cs +++ b/src/Uno.UI/UI/Xaml/Application.iOS.cs @@ -29,12 +29,9 @@ public partial class Application : UIApplicationDelegate private bool _preventSecondaryActivationHandling; - public Application() + partial void InitializePartial() { - Current = this; - ApplicationLanguages.ApplyCulture(); SetCurrentLanguage(); - InitializeSystemTheme(); SubscribeBackgroundNotifications(); } diff --git a/src/Uno.UI/UI/Xaml/Application.macOS.cs b/src/Uno.UI/UI/Xaml/Application.macOS.cs index f2a950aeadc7..2022f5e35a5a 100644 --- a/src/Uno.UI/UI/Xaml/Application.macOS.cs +++ b/src/Uno.UI/UI/Xaml/Application.macOS.cs @@ -33,12 +33,9 @@ static partial void InitializePartialStatic() ApiExtensibility.Register(typeof(IUnoCorePointerInputSource), host => new MacOSPointerInputSource((Uno.UI.Controls.Window)((Windows.UI.Xaml.Window)host).NativeWindow)); } - public Application() + partial void InitializePartial() { - Current = this; - ApplicationLanguages.ApplyCulture(); SetCurrentLanguage(); - InitializeSystemTheme(); SubscribeBackgroundNotifications(); } diff --git a/src/Uno.UI/UI/Xaml/Application.reference.cs b/src/Uno.UI/UI/Xaml/Application.reference.cs deleted file mode 100644 index 72fb498eaa3c..000000000000 --- a/src/Uno.UI/UI/Xaml/Application.reference.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Windows.Globalization; - -namespace Windows.UI.Xaml; - -public partial class Application -{ - public Application() - { - Current = this; - ApplicationLanguages.ApplyCulture(); - InitializeSystemTheme(); - } -} diff --git a/src/Uno.UI/UI/Xaml/Application.skia.cs b/src/Uno.UI/UI/Xaml/Application.skia.cs index 406057cb7b86..1892e0bd3724 100644 --- a/src/Uno.UI/UI/Xaml/Application.skia.cs +++ b/src/Uno.UI/UI/Xaml/Application.skia.cs @@ -22,13 +22,9 @@ public partial class Application : IApplicationEvents private static bool _startInvoked; private static string _arguments = ""; - public Application() + partial void InitializePartial() { - Current = this; - Package.SetEntryAssembly(this.GetType().Assembly); - ApplicationLanguages.ApplyCulture(); SetCurrentLanguage(); - InitializeSystemTheme(); if (!_startInvoked) { diff --git a/src/Uno.UI/UI/Xaml/Application.unittests.cs b/src/Uno.UI/UI/Xaml/Application.unittests.cs deleted file mode 100644 index 72fb498eaa3c..000000000000 --- a/src/Uno.UI/UI/Xaml/Application.unittests.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Windows.Globalization; - -namespace Windows.UI.Xaml; - -public partial class Application -{ - public Application() - { - Current = this; - ApplicationLanguages.ApplyCulture(); - InitializeSystemTheme(); - } -} diff --git a/src/Uno.UI/UI/Xaml/Application.wasm.cs b/src/Uno.UI/UI/Xaml/Application.wasm.cs index bf84a04efd44..f158d9e45b52 100644 --- a/src/Uno.UI/UI/Xaml/Application.wasm.cs +++ b/src/Uno.UI/UI/Xaml/Application.wasm.cs @@ -37,18 +37,13 @@ public partial class Application { private static bool _startInvoked; - public Application() + partial void InitializePartial() { if (!_startInvoked) { throw new InvalidOperationException("The application must be started using Application.Start first, e.g. Windows.UI.Xaml.Application.Start(_ => new App());"); } - Current = this; - InitializeSystemTheme(); - Package.SetEntryAssembly(this.GetType().Assembly); - ApplicationLanguages.ApplyCulture(); - global::Uno.Foundation.Extensibility.ApiExtensibility.Register( typeof(global::Windows.ApplicationModel.DataTransfer.DragDrop.Core.IDragDropExtension), o => global::Windows.ApplicationModel.DataTransfer.DragDrop.Core.DragDropExtension.GetForCurrentView()); From d8f43ea00f555d53461388b8ff899bf15fd19456 Mon Sep 17 00:00:00 2001 From: Martin Zikmund Date: Tue, 20 Jun 2023 10:45:38 +0200 Subject: [PATCH 06/29] chore: Update package properties and defaults --- src/Uno.UWP/ApplicationModel/PackageId.cs | 47 +------- .../ApplicationModel/PackageId.defaults.cs | 42 ++++++++ .../ApplicationModel/PackageId.iOSmacOS.cs | 62 +++++------ .../ApplicationModel/PackageId.netstd.cs | 10 ++ .../ApplicationModel/PackageId.other.cs | 39 ------- .../ApplicationModel/PackageVersion.cs | 101 ++++++++++-------- 6 files changed, 146 insertions(+), 155 deletions(-) create mode 100644 src/Uno.UWP/ApplicationModel/PackageId.defaults.cs create mode 100644 src/Uno.UWP/ApplicationModel/PackageId.netstd.cs delete mode 100644 src/Uno.UWP/ApplicationModel/PackageId.other.cs diff --git a/src/Uno.UWP/ApplicationModel/PackageId.cs b/src/Uno.UWP/ApplicationModel/PackageId.cs index 2ef3819e3da7..efe5986d3559 100644 --- a/src/Uno.UWP/ApplicationModel/PackageId.cs +++ b/src/Uno.UWP/ApplicationModel/PackageId.cs @@ -1,47 +1,8 @@ -#if (__IOS__ || __ANDROID__ || __MACOS__) -#pragma warning disable 108 // new keyword hiding -#pragma warning disable 114 // new keyword hiding -using System.Reflection; -using Uno.Extensions; -using ProcessorArchitecture = Windows.System.ProcessorArchitecture; +namespace Windows.ApplicationModel; -namespace Windows.ApplicationModel +public sealed partial class PackageId { - public sealed partial class PackageId - { - internal PackageId() => InitializePlatform(); + internal PackageId() => InitializePlatform(); - partial void InitializePlatform(); - - [Uno.NotImplemented] - public ProcessorArchitecture Architecture => ProcessorArchitecture.Unknown; - - [global::Uno.NotImplemented("__WASM__")] - public string FamilyName => "Unknown"; - - [global::Uno.NotImplemented("__WASM__")] - public string FullName => "Unknown"; - - [global::Uno.NotImplemented("__WASM__")] - public string Name => "Unknown"; - - [global::Uno.NotImplemented("__WASM__")] - public PackageVersion Version => new PackageVersion(Assembly.GetExecutingAssembly().GetVersionNumber()); - - [Uno.NotImplemented] - public string Publisher => "Unknown"; - - [Uno.NotImplemented] - public string PublisherId => "Unknown"; - - [Uno.NotImplemented] - public string ResourceId => "Unknown"; - - [Uno.NotImplemented] - public string Author => "Unknown"; - - [Uno.NotImplemented] - public string ProductId => "Unknown"; - } + partial void InitializePlatform(); } -#endif diff --git a/src/Uno.UWP/ApplicationModel/PackageId.defaults.cs b/src/Uno.UWP/ApplicationModel/PackageId.defaults.cs new file mode 100644 index 000000000000..a2ce79dbe536 --- /dev/null +++ b/src/Uno.UWP/ApplicationModel/PackageId.defaults.cs @@ -0,0 +1,42 @@ +using System.Reflection; +using Uno.Extensions; +using ProcessorArchitecture = Windows.System.ProcessorArchitecture; + +namespace Windows.ApplicationModel; + +partial class PackageId +{ + [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")] + public ProcessorArchitecture Architecture => ProcessorArchitecture.Unknown; + + [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")] + public string PublisherId => "Unknown"; + + [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")] + public string ResourceId => "Unknown"; + + [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")] + public string Author => "Unknown"; + + [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")] + public string ProductId => "Unknown"; + +#if !__ANDROID__ && __IOS__ && !__MACOS__ && !__SKIA__ && !__WASM__ + [global::Uno.NotImplemented("IS_UNIT_TESTS")] + public string FamilyName => "Unknown"; + + [global::Uno.NotImplemented("IS_UNIT_TESTS")] + public string FullName => "Unknown"; + + [global::Uno.NotImplemented("IS_UNIT_TESTS")] + public string Name { get; internal set; } = "Unknown"; + + [global::Uno.NotImplemented("IS_UNIT_TESTS")] + public PackageVersion Version => new PackageVersion(Assembly.GetExecutingAssembly().GetVersionNumber()); +#endif + +#if !__WASM__ && !__SKIA__ && !__NETSTD_REFERENCE__ + [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__MACOS__")] + public string Publisher => "Unknown"; +#endif +} diff --git a/src/Uno.UWP/ApplicationModel/PackageId.iOSmacOS.cs b/src/Uno.UWP/ApplicationModel/PackageId.iOSmacOS.cs index 75c380549c4a..f802947a969b 100644 --- a/src/Uno.UWP/ApplicationModel/PackageId.iOSmacOS.cs +++ b/src/Uno.UWP/ApplicationModel/PackageId.iOSmacOS.cs @@ -1,46 +1,46 @@ using Foundation; using SystemVersion = global::System.Version; -namespace Windows.ApplicationModel +namespace Windows.ApplicationModel; + +public partial class PackageId { - public partial class PackageId - { - private const string BundleIdentifierKey = "CFBundleIdentifier"; - private const string BundleShortVersionKey = "CFBundleShortVersionString"; - private const string BundleVersionKey = "CFBundleVersion"; + private const string BundleIdentifierKey = "CFBundleIdentifier"; + private const string BundleShortVersionKey = "CFBundleShortVersionString"; + private const string BundleVersionKey = "CFBundleVersion"; - private const string DefaultVersionString = "0.0"; + private const string DefaultVersionString = "0.0"; - public string FamilyName => NSBundle.MainBundle.InfoDictionary[BundleIdentifierKey]?.ToString() ?? string.Empty; + public string FamilyName => NSBundle.MainBundle.InfoDictionary[BundleIdentifierKey]?.ToString() ?? string.Empty; - public string FullName => - $"{NSBundle.MainBundle.InfoDictionary[BundleIdentifierKey]?.ToString() ?? string.Empty}_" + - $"{NSBundle.MainBundle.InfoDictionary[BundleVersionKey]?.ToString() ?? DefaultVersionString}"; + public string FullName => + $"{NSBundle.MainBundle.InfoDictionary[BundleIdentifierKey]?.ToString() ?? string.Empty}_" + + $"{NSBundle.MainBundle.InfoDictionary[BundleVersionKey]?.ToString() ?? DefaultVersionString}"; - public string Name => NSBundle.MainBundle.InfoDictionary[BundleIdentifierKey].ToString(); + public string Name => NSBundle.MainBundle.InfoDictionary[BundleIdentifierKey].ToString(); - /// - /// Implementation based on . - /// - public PackageVersion Version + /// + /// Implementation based on . + /// + public PackageVersion Version + { + get { - get + var shortVersion = NSBundle.MainBundle.InfoDictionary[BundleShortVersionKey]?.ToString() ?? DefaultVersionString; + var bundleVersion = NSBundle.MainBundle.InfoDictionary[BundleVersionKey]?.ToString() ?? DefaultVersionString; + // Short version is the user-displayed version, use if possible + if (SystemVersion.TryParse(shortVersion, out var userVersion)) + { + return new PackageVersion(userVersion); + } + // If user-displayed version is not set, use the actual app version + if (SystemVersion.TryParse(bundleVersion, out var appVersion)) { - var shortVersion = NSBundle.MainBundle.InfoDictionary[BundleShortVersionKey]?.ToString() ?? DefaultVersionString; - var bundleVersion = NSBundle.MainBundle.InfoDictionary[BundleVersionKey]?.ToString() ?? DefaultVersionString; - // Short version is the user-displayed version, use if possible - if (SystemVersion.TryParse(shortVersion, out var userVersion)) - { - return new PackageVersion(userVersion); - } - // If user-displayed version is not set, use the actual app version - if (SystemVersion.TryParse(bundleVersion, out var appVersion)) - { - return new PackageVersion(appVersion); - } - // Fallback to default - return new PackageVersion(); + return new PackageVersion(appVersion); } + // Fallback to default + return new PackageVersion(); } } } +endif diff --git a/src/Uno.UWP/ApplicationModel/PackageId.netstd.cs b/src/Uno.UWP/ApplicationModel/PackageId.netstd.cs new file mode 100644 index 000000000000..11c577467393 --- /dev/null +++ b/src/Uno.UWP/ApplicationModel/PackageId.netstd.cs @@ -0,0 +1,10 @@ +namespace Windows.ApplicationModel; + +public sealed partial class PackageId +{ + public string Name { get; internal set; } = ""; + + public PackageVersion Version { get; internal set; } + + public string Publisher { get; internal set; } = ""; +} diff --git a/src/Uno.UWP/ApplicationModel/PackageId.other.cs b/src/Uno.UWP/ApplicationModel/PackageId.other.cs deleted file mode 100644 index 7c9c365d6237..000000000000 --- a/src/Uno.UWP/ApplicationModel/PackageId.other.cs +++ /dev/null @@ -1,39 +0,0 @@ -#if !(__IOS__ || __ANDROID__ || __MACOS__) -using ProcessorArchitecture = Windows.System.ProcessorArchitecture; - -namespace Windows.ApplicationModel; - -public sealed partial class PackageId -{ - internal PackageId() - { - } - - [Uno.NotImplemented] - public ProcessorArchitecture Architecture => ProcessorArchitecture.Unknown; - - [global::Uno.NotImplemented("__WASM__, __SKIA__")] - public string FamilyName => "Unknown"; - - [global::Uno.NotImplemented("__WASM__, __SKIA__")] - public string FullName => "Unknown"; - - public string Name { get; internal set; } = ""; - - public PackageVersion Version { get; internal set; } - - public string Publisher { get; internal set; } = ""; - - [Uno.NotImplemented] - public string PublisherId => "Unknown"; - - [Uno.NotImplemented] - public string ResourceId => "Unknown"; - - [Uno.NotImplemented] - public string Author => "Unknown"; - - [Uno.NotImplemented] - public string ProductId => "Unknown"; -} -#endif diff --git a/src/Uno.UWP/ApplicationModel/PackageVersion.cs b/src/Uno.UWP/ApplicationModel/PackageVersion.cs index 794c72f267e9..6bc3ee462a7d 100644 --- a/src/Uno.UWP/ApplicationModel/PackageVersion.cs +++ b/src/Uno.UWP/ApplicationModel/PackageVersion.cs @@ -1,48 +1,65 @@ using System; -namespace Windows.ApplicationModel +namespace Windows.ApplicationModel; + +/// +/// Represents the package version info. +/// +public partial struct PackageVersion : IEquatable { - public partial struct PackageVersion : IEquatable + internal PackageVersion(ushort major) + { + Major = major; + Minor = 0; + Build = 0; + Revision = 0; + } + + internal PackageVersion(global::System.Version version) + { + Major = (ushort)(version.Major >= 0 ? version.Major : 0); + Minor = (ushort)(version.Minor >= 0 ? version.Minor : 0); + Build = (ushort)(version.Build >= 0 ? version.Build : 0); + Revision = (ushort)(version.Revision >= 0 ? version.Revision : 0); + } + + // NOTE: Equality implementation should be modified if a new field/property is added. + + /// + /// The major version number of the package. + /// + public ushort Major; + + /// + /// The minor version number of the package. + /// + public ushort Minor; + + /// + /// The build version number of the package. + /// + public ushort Build; + + /// + /// The revision version number of the package. + /// + public ushort Revision; + + #region Equality Members + public override bool Equals(object obj) => obj is PackageVersion version && Equals(version); + public bool Equals(PackageVersion other) => Major == other.Major && Minor == other.Minor && Build == other.Build && Revision == other.Revision; + + public override int GetHashCode() { - internal PackageVersion(ushort major) - { - Major = major; - Minor = 0; - Build = 0; - Revision = 0; - } - - internal PackageVersion(global::System.Version version) - { - Major = (ushort)(version.Major >= 0 ? version.Major : 0); - Minor = (ushort)(version.Minor >= 0 ? version.Minor : 0); - Build = (ushort)(version.Build >= 0 ? version.Build : 0); - Revision = (ushort)(version.Revision >= 0 ? version.Revision : 0); - } - - // NOTE: Equality implementation should be modified if a new field/property is added. - - public ushort Major; - public ushort Minor; - public ushort Build; - public ushort Revision; - - #region Equality Members - public override bool Equals(object obj) => obj is PackageVersion version && Equals(version); - public bool Equals(PackageVersion other) => Major == other.Major && Minor == other.Minor && Build == other.Build && Revision == other.Revision; - - public override int GetHashCode() - { - var hashCode = -1452750829; - hashCode = hashCode * -1521134295 + Major.GetHashCode(); - hashCode = hashCode * -1521134295 + Minor.GetHashCode(); - hashCode = hashCode * -1521134295 + Build.GetHashCode(); - hashCode = hashCode * -1521134295 + Revision.GetHashCode(); - return hashCode; - } - - public static bool operator ==(PackageVersion left, PackageVersion right) => left.Equals(right); - public static bool operator !=(PackageVersion left, PackageVersion right) => !left.Equals(right); - #endregion + var hashCode = -1452750829; + hashCode = hashCode * -1521134295 + Major.GetHashCode(); + hashCode = hashCode * -1521134295 + Minor.GetHashCode(); + hashCode = hashCode * -1521134295 + Build.GetHashCode(); + hashCode = hashCode * -1521134295 + Revision.GetHashCode(); + return hashCode; } + + public static bool operator ==(PackageVersion left, PackageVersion right) => left.Equals(right); + public static bool operator !=(PackageVersion left, PackageVersion right) => !left.Equals(right); + #endregion } From fa367c48a604fa7257558d6345b45892eb3b4ed4 Mon Sep 17 00:00:00 2001 From: Martin Zikmund Date: Tue, 20 Jun 2023 15:23:15 +0200 Subject: [PATCH 07/29] chore: Adjust directive --- src/Uno.UWP/ApplicationModel/PackageId.iOSmacOS.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Uno.UWP/ApplicationModel/PackageId.iOSmacOS.cs b/src/Uno.UWP/ApplicationModel/PackageId.iOSmacOS.cs index f802947a969b..2950709616ff 100644 --- a/src/Uno.UWP/ApplicationModel/PackageId.iOSmacOS.cs +++ b/src/Uno.UWP/ApplicationModel/PackageId.iOSmacOS.cs @@ -43,4 +43,4 @@ public PackageVersion Version } } } -endif +#endif From 9b23f3cf411f625465867fd6cccd85903e3e92c0 Mon Sep 17 00:00:00 2001 From: Martin Zikmund Date: Wed, 21 Jun 2023 08:09:09 +0200 Subject: [PATCH 08/29] chore: Adjust properties --- src/Uno.UWP/ApplicationModel/Package.Other.cs | 62 +++++++++---------- .../ApplicationModel/PackageId.defaults.cs | 4 +- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/Uno.UWP/ApplicationModel/Package.Other.cs b/src/Uno.UWP/ApplicationModel/Package.Other.cs index b6991b127990..d3fa4efe3d84 100644 --- a/src/Uno.UWP/ApplicationModel/Package.Other.cs +++ b/src/Uno.UWP/ApplicationModel/Package.Other.cs @@ -56,53 +56,51 @@ private void ParsePackageManifest() return; } - var manifest = _entryAssembly.GetManifestResourceStream(PackageManifestName); - - if (manifest is not null) + if (_entryAssembly.GetManifestResourceStream(PackageManifestName) is not { } manifest) { - try + if (this.Log().IsEnabled(Uno.Foundation.Logging.LogLevel.Debug)) { - var doc = new XmlDocument(); - doc.Load(manifest); - - var nsmgr = new XmlNamespaceManager(doc.NameTable); - nsmgr.AddNamespace("d", "http://schemas.microsoft.com/appx/manifest/foundation/windows10"); + this.Log().Debug($"Skipping manifest reading, unable to find [{PackageManifestName}]"); + } - DisplayName = doc.SelectSingleNode("/d:Package/d:Properties/d:DisplayName", nsmgr)?.InnerText ?? ""; + return; + } - var logoUri = doc.SelectSingleNode("/d:Package/d:Properties/d:Logo", nsmgr)?.InnerText ?? ""; - if (Uri.TryCreate(logoUri, UriKind.RelativeOrAbsolute, out var logo)) - { - Logo = logo; - } + try + { + var doc = new XmlDocument(); + doc.Load(manifest); - var idNode = doc.SelectSingleNode("/d:Package/d:Identity", nsmgr); - if (idNode is not null) - { - Id.Name = idNode.Attributes?.GetNamedItem("Name")?.Value ?? ""; + var nsmgr = new XmlNamespaceManager(doc.NameTable); + nsmgr.AddNamespace("d", "http://schemas.microsoft.com/appx/manifest/foundation/windows10"); - var versionString = idNode.Attributes?.GetNamedItem("Version")?.Value ?? ""; - if (Version.TryParse(versionString, out var version)) - { - Id.Version = new PackageVersion(version); - } + DisplayName = doc.SelectSingleNode("/d:Package/d:Properties/d:DisplayName", nsmgr)?.InnerText ?? ""; - Id.Publisher = idNode.Attributes?.GetNamedItem("Publisher")?.Value ?? ""; - } + var logoUri = doc.SelectSingleNode("/d:Package/d:Properties/d:Logo", nsmgr)?.InnerText ?? ""; + if (Uri.TryCreate(logoUri, UriKind.RelativeOrAbsolute, out var logo)) + { + Logo = logo; } - catch (Exception ex) + + var idNode = doc.SelectSingleNode("/d:Package/d:Identity", nsmgr); + if (idNode is not null) { - if (this.Log().IsEnabled(Uno.Foundation.Logging.LogLevel.Error)) + Id.Name = idNode.Attributes?.GetNamedItem("Name")?.Value ?? ""; + + var versionString = idNode.Attributes?.GetNamedItem("Version")?.Value ?? ""; + if (Version.TryParse(versionString, out var version)) { - this.Log().Error($"Failed to read manifest [{PackageManifestName}]", ex); + Id.Version = new PackageVersion(version); } + + Id.Publisher = idNode.Attributes?.GetNamedItem("Publisher")?.Value ?? ""; } } - else + catch (Exception ex) { - if (this.Log().IsEnabled(Uno.Foundation.Logging.LogLevel.Debug)) + if (this.Log().IsEnabled(Uno.Foundation.Logging.LogLevel.Error)) { - this.Log().Debug($"Skipping manifest reading, unable to find [{PackageManifestName}]"); + this.Log().Error($"Failed to read manifest [{PackageManifestName}]", ex); } } } diff --git a/src/Uno.UWP/ApplicationModel/PackageId.defaults.cs b/src/Uno.UWP/ApplicationModel/PackageId.defaults.cs index a2ce79dbe536..dbe31fe1377a 100644 --- a/src/Uno.UWP/ApplicationModel/PackageId.defaults.cs +++ b/src/Uno.UWP/ApplicationModel/PackageId.defaults.cs @@ -21,13 +21,15 @@ partial class PackageId [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")] public string ProductId => "Unknown"; -#if !__ANDROID__ && __IOS__ && !__MACOS__ && !__SKIA__ && !__WASM__ +#if !__ANDROID__ && !__IOS__ && !__MACOS__ [global::Uno.NotImplemented("IS_UNIT_TESTS")] public string FamilyName => "Unknown"; [global::Uno.NotImplemented("IS_UNIT_TESTS")] public string FullName => "Unknown"; +#endif +#if !__ANDROID__ && !__IOS__ && !__MACOS__ && !__WASM__ && !__SKIA__ && !__NETSTD_REFERENCE__ [global::Uno.NotImplemented("IS_UNIT_TESTS")] public string Name { get; internal set; } = "Unknown"; From 48d64da22085db2f845e5b8b73741332e89785ce Mon Sep 17 00:00:00 2001 From: Martin Zikmund Date: Wed, 21 Jun 2023 09:38:55 +0200 Subject: [PATCH 09/29] chore: Remove duplicate constructor --- src/Uno.UI/UI/Xaml/Application.Android.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Uno.UI/UI/Xaml/Application.Android.cs b/src/Uno.UI/UI/Xaml/Application.Android.cs index ded7ce01509c..25eeff21d478 100644 --- a/src/Uno.UI/UI/Xaml/Application.Android.cs +++ b/src/Uno.UI/UI/Xaml/Application.Android.cs @@ -19,12 +19,9 @@ namespace Windows.UI.Xaml { public partial class Application { - public Application() + partial void InitializePartial() { Window.Current.ToString(); - Current = this; - ApplicationLanguages.ApplyCulture(); - InitializeSystemTheme(); PermissionsHelper.Initialize(); } From 04e95b4677fde16011e38099aca526b5fc5265b5 Mon Sep 17 00:00:00 2001 From: Martin Zikmund Date: Wed, 21 Jun 2023 23:10:11 +0200 Subject: [PATCH 10/29] chore: Adjust setter for Version and Publisher --- src/Uno.UWP/ApplicationModel/PackageId.defaults.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Uno.UWP/ApplicationModel/PackageId.defaults.cs b/src/Uno.UWP/ApplicationModel/PackageId.defaults.cs index dbe31fe1377a..f84bbaaa776a 100644 --- a/src/Uno.UWP/ApplicationModel/PackageId.defaults.cs +++ b/src/Uno.UWP/ApplicationModel/PackageId.defaults.cs @@ -29,16 +29,16 @@ partial class PackageId public string FullName => "Unknown"; #endif -#if !__ANDROID__ && !__IOS__ && !__MACOS__ && !__WASM__ && !__SKIA__ && !__NETSTD_REFERENCE__ +#if IS_UNIT_TESTS [global::Uno.NotImplemented("IS_UNIT_TESTS")] public string Name { get; internal set; } = "Unknown"; [global::Uno.NotImplemented("IS_UNIT_TESTS")] - public PackageVersion Version => new PackageVersion(Assembly.GetExecutingAssembly().GetVersionNumber()); + public PackageVersion Version { get; internal set; } = new PackageVersion(Assembly.GetExecutingAssembly().GetVersionNumber()); #endif #if !__WASM__ && !__SKIA__ && !__NETSTD_REFERENCE__ [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__MACOS__")] - public string Publisher => "Unknown"; + public string Publisher { get; internal set; } = "Unknown"; #endif } From 2f197c66d765a56123d4584487f895a52c925a5a Mon Sep 17 00:00:00 2001 From: Martin Zikmund Date: Fri, 7 Jul 2023 16:59:36 +0200 Subject: [PATCH 11/29] chore: Adjust for WASM lifecycle --- src/Uno.UWP/Storage/ApplicationData.cs | 6 +- .../Storage/ApplicationData.reference.cs | 27 ++++--- src/Uno.UWP/Storage/ApplicationData.skia.cs | 78 +++++++++---------- src/Uno.UWP/Storage/ApplicationData.wasm.cs | 63 ++++++++------- 4 files changed, 85 insertions(+), 89 deletions(-) diff --git a/src/Uno.UWP/Storage/ApplicationData.cs b/src/Uno.UWP/Storage/ApplicationData.cs index ed015376d5a7..6173deb47616 100644 --- a/src/Uno.UWP/Storage/ApplicationData.cs +++ b/src/Uno.UWP/Storage/ApplicationData.cs @@ -12,8 +12,6 @@ public sealed partial class ApplicationData private ApplicationData() { - PartialCtor(); - LocalFolder = new StorageFolder(GetLocalFolder()); RoamingFolder = new StorageFolder(GetRoamingFolder()); SharedLocalFolder = new StorageFolder(".shared", GetSharedLocalFolder()); @@ -22,9 +20,11 @@ private ApplicationData() LocalSettings = new ApplicationDataContainer(this, "Local", ApplicationDataLocality.Local); RoamingSettings = new ApplicationDataContainer(this, "Roaming", ApplicationDataLocality.Roaming); + + InitializePartial(); } - partial void PartialCtor(); + partial void InitializePartial(); public StorageFolder LocalFolder { get; } diff --git a/src/Uno.UWP/Storage/ApplicationData.reference.cs b/src/Uno.UWP/Storage/ApplicationData.reference.cs index d5727effc4f8..c48ae18581ec 100644 --- a/src/Uno.UWP/Storage/ApplicationData.reference.cs +++ b/src/Uno.UWP/Storage/ApplicationData.reference.cs @@ -1,23 +1,22 @@ using System; using System.IO; -namespace Windows.Storage +namespace Windows.Storage; + +partial class ApplicationData { - partial class ApplicationData - { - private static string GetLocalCacheFolder() - => Path.GetTempPath(); + private static string GetLocalCacheFolder() + => Path.GetTempPath(); - private static string GetTemporaryFolder() - => Path.GetTempPath(); + private static string GetTemporaryFolder() + => Path.GetTempPath(); - private static string GetLocalFolder() - => AppDomain.CurrentDomain.BaseDirectory; + private static string GetLocalFolder() + => AppDomain.CurrentDomain.BaseDirectory; - private static string GetRoamingFolder() - => Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + private static string GetRoamingFolder() + => Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); - private static string GetSharedLocalFolder() - => Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); - } + private static string GetSharedLocalFolder() + => Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); } diff --git a/src/Uno.UWP/Storage/ApplicationData.skia.cs b/src/Uno.UWP/Storage/ApplicationData.skia.cs index 77873076f2cf..fbdb46bd5c8b 100644 --- a/src/Uno.UWP/Storage/ApplicationData.skia.cs +++ b/src/Uno.UWP/Storage/ApplicationData.skia.cs @@ -19,26 +19,24 @@ partial class ApplicationData private const string RoamingFolderName = "RoamingState"; private const string SettingsFolderName = "Settings"; - private static string _appSpecificSubpath = null!; + private static string? _appSpecificSubpath; - partial void PartialCtor() => EnsureAppSubpath(); + private string GetLocalCacheFolder() => EnsurePath(Path.Combine(Path.GetTempPath(), GetAppSpecificSubPath(), LocalCacheFolderName)); - private string GetLocalCacheFolder() => EnsurePath(Path.Combine(Path.GetTempPath(), _appSpecificSubpath, LocalCacheFolderName)); - - private string GetTemporaryFolder() => EnsurePath(Path.Combine(Path.GetTempPath(), _appSpecificSubpath, TemporaryFolderName)); + private string GetTemporaryFolder() => EnsurePath(Path.Combine(Path.GetTempPath(), GetAppSpecificSubPath(), TemporaryFolderName)); private string GetLocalFolder() => // Uses XDG_DATA_HOME on Unix: https://github.com/dotnet/runtime/blob/b5705587347d29d79cec830dc22b389e1ad9a9e0/src/libraries/System.Private.CoreLib/src/System/Environment.GetFolderPathCore.Unix.cs#L105 - EnsurePath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), _appSpecificSubpath, LocalFolderName)); + EnsurePath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), GetAppSpecificSubPath(), LocalFolderName)); private string GetRoamingFolder() => - EnsurePath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), _appSpecificSubpath, RoamingFolderName)); + EnsurePath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), GetAppSpecificSubPath(), RoamingFolderName)); private string GetSharedLocalFolder() => - EnsurePath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), _appSpecificSubpath, SharedLocalFolderName)); + EnsurePath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), GetAppSpecificSubPath(), SharedLocalFolderName)); internal string GetSettingsFolderPath() => - EnsurePath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), _appSpecificSubpath, SettingsFolderName)); + EnsurePath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), GetAppSpecificSubPath(), SettingsFolderName)); private string EnsurePath(string path) { @@ -47,48 +45,48 @@ private string EnsurePath(string path) } [MemberNotNull(nameof(_appSpecificSubpath))] - private static void EnsureAppSubpath() + private string GetAppSpecificSubPath() { - if (_appSpecificSubpath is not null) - { - return; - } - - if (!Package.IsManifestInitialized) + if (_appSpecificSubpath is null) { - throw new InvalidOperationException("The Package.Id is not initialized yet."); - } + if (!Package.IsManifestInitialized) + { + throw new InvalidOperationException("The Package.Id is not initialized yet."); + } - var appName = Package.Current.Id.Name; - var appNameSafe = GetFileNameSafeString(appName); + var appName = Package.Current.Id.Name; + var appNameSafe = GetFileNameSafeString(appName); - var publisherDistinguishedName = Package.Current.Id.Publisher; - string? publisherName = null; - if (!string.IsNullOrEmpty(publisherDistinguishedName)) - { - var parts = publisherDistinguishedName.Split(','); - if (parts.FirstOrDefault(p => p.StartsWith(DistinguishedNameOrganizationPrefix, StringComparison.OrdinalIgnoreCase)) is { } organizationPart) + var publisherDistinguishedName = Package.Current.Id.Publisher; + string? publisherName = null; + if (!string.IsNullOrEmpty(publisherDistinguishedName)) { - publisherName = organizationPart.Substring(DistinguishedNameOrganizationPrefix.Length); + var parts = publisherDistinguishedName.Split(','); + if (parts.FirstOrDefault(p => p.StartsWith(DistinguishedNameOrganizationPrefix, StringComparison.OrdinalIgnoreCase)) is { } organizationPart) + { + publisherName = organizationPart.Substring(DistinguishedNameOrganizationPrefix.Length); + } + + if (string.IsNullOrEmpty(publisherName) && + parts.FirstOrDefault(p => p.StartsWith(DistinguishedNameCommonNamePrefix, StringComparison.OrdinalIgnoreCase)) is { } commonNamePart) + { + publisherName = commonNamePart.Substring(DistinguishedNameCommonNamePrefix.Length); + } } - if (string.IsNullOrEmpty(publisherName) && - parts.FirstOrDefault(p => p.StartsWith(DistinguishedNameCommonNamePrefix, StringComparison.OrdinalIgnoreCase)) is { } commonNamePart) + var publisherNameSafe = !string.IsNullOrEmpty(publisherName) ? GetFileNameSafeString(publisherName) : null; + + if (publisherNameSafe is not null) + { + _appSpecificSubpath = Path.Combine(publisherNameSafe, appNameSafe); + } + else { - publisherName = commonNamePart.Substring(DistinguishedNameCommonNamePrefix.Length); + _appSpecificSubpath = appNameSafe; } } - var publisherNameSafe = !string.IsNullOrEmpty(publisherName) ? GetFileNameSafeString(publisherName) : null; - - if (publisherNameSafe is not null) - { - _appSpecificSubpath = Path.Combine(publisherNameSafe, appNameSafe); - } - else - { - _appSpecificSubpath = appNameSafe; - } + return _appSpecificSubpath; } private static string GetFileNameSafeString(string fileName) diff --git a/src/Uno.UWP/Storage/ApplicationData.wasm.cs b/src/Uno.UWP/Storage/ApplicationData.wasm.cs index 30a77f1710ef..30b621d3e249 100644 --- a/src/Uno.UWP/Storage/ApplicationData.wasm.cs +++ b/src/Uno.UWP/Storage/ApplicationData.wasm.cs @@ -4,39 +4,38 @@ using Uno.Foundation; using Uno.Foundation.Interop; -namespace Windows.Storage +namespace Windows.Storage; + +partial class ApplicationData { - partial class ApplicationData + internal static void Init() { - internal static void Init() - { - // Nothing to do here, this is only a way to explicitly poke the 'ApplicationData' so the - // 'Current' is instantiated and the 'PartialCtor' is invoked. - } - - partial void PartialCtor() - { - StorageFolder.MakePersistent( - LocalFolder, - RoamingFolder, - // TemporaryFolder.Path: No needs to persist it! - // LocalCacheFolder.Path: Usually this does needs to be persisted, so keep it disable by default for perf consideration - SharedLocalFolder); - } - - private static string GetLocalCacheFolder() - => WebAssemblyRuntime.IsWebAssembly ? "/cache" : @".\cache"; - - private static string GetTemporaryFolder() - => WebAssemblyRuntime.IsWebAssembly ? "/temp" : @".\temp"; - - private static string GetLocalFolder() - => WebAssemblyRuntime.IsWebAssembly ? "/local" : @".\local"; - - private static string GetRoamingFolder() - => WebAssemblyRuntime.IsWebAssembly ? "/roaming" : @".\roaming"; - - private static string GetSharedLocalFolder() - => WebAssemblyRuntime.IsWebAssembly ? "/shared" : @".\shared"; + // Nothing to do here, this is only a way to explicitly poke the 'ApplicationData' so the + // 'Current' is instantiated and the 'InitializePartial' is invoked. } + + partial void InitializePartial() + { + StorageFolder.MakePersistent( + LocalFolder, + RoamingFolder, + // TemporaryFolder.Path: No needs to persist it! + // LocalCacheFolder.Path: Usually this does needs to be persisted, so keep it disable by default for perf consideration + SharedLocalFolder); + } + + private static string GetLocalCacheFolder() + => WebAssemblyRuntime.IsWebAssembly ? "/cache" : @".\cache"; + + private static string GetTemporaryFolder() + => WebAssemblyRuntime.IsWebAssembly ? "/temp" : @".\temp"; + + private static string GetLocalFolder() + => WebAssemblyRuntime.IsWebAssembly ? "/local" : @".\local"; + + private static string GetRoamingFolder() + => WebAssemblyRuntime.IsWebAssembly ? "/roaming" : @".\roaming"; + + private static string GetSharedLocalFolder() + => WebAssemblyRuntime.IsWebAssembly ? "/shared" : @".\shared"; } From 9877444b45e3714d48c52fda2ff0d0022aaa09c5 Mon Sep 17 00:00:00 2001 From: Martin Zikmund Date: Fri, 7 Jul 2023 22:44:09 +0200 Subject: [PATCH 12/29] chore: Adjust Logo default value --- src/Uno.UWP/ApplicationModel/Package.Other.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Uno.UWP/ApplicationModel/Package.Other.cs b/src/Uno.UWP/ApplicationModel/Package.Other.cs index d3fa4efe3d84..40c26a03ee0c 100644 --- a/src/Uno.UWP/ApplicationModel/Package.Other.cs +++ b/src/Uno.UWP/ApplicationModel/Package.Other.cs @@ -39,7 +39,7 @@ private string GetInstalledPath() public string DisplayName { get; private set; } = ""; - public Uri Logo { get; private set; } = new Uri("ms-appx://logo", UriKind.RelativeOrAbsolute); + public Uri? Logo { get; set; } = null; internal static void SetEntryAssembly(Assembly entryAssembly) { From 15caaeb799c272ed2893a2f9d0a49c74aa1ef56e Mon Sep 17 00:00:00 2001 From: Martin Zikmund Date: Sat, 8 Jul 2023 08:05:19 +0200 Subject: [PATCH 13/29] chore: Remove directive --- src/Uno.UWP/ApplicationModel/PackageId.iOSmacOS.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Uno.UWP/ApplicationModel/PackageId.iOSmacOS.cs b/src/Uno.UWP/ApplicationModel/PackageId.iOSmacOS.cs index 2950709616ff..66d11e13caa3 100644 --- a/src/Uno.UWP/ApplicationModel/PackageId.iOSmacOS.cs +++ b/src/Uno.UWP/ApplicationModel/PackageId.iOSmacOS.cs @@ -43,4 +43,3 @@ public PackageVersion Version } } } -#endif From 7ebf20460d7e6f5f1907c5bb2895059d29894c05 Mon Sep 17 00:00:00 2001 From: Martin Zikmund Date: Sat, 8 Jul 2023 12:56:02 +0200 Subject: [PATCH 14/29] chore: Avoid initializing to default value --- src/Uno.UWP/ApplicationModel/Package.Other.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Uno.UWP/ApplicationModel/Package.Other.cs b/src/Uno.UWP/ApplicationModel/Package.Other.cs index 40c26a03ee0c..1bcc5c57f597 100644 --- a/src/Uno.UWP/ApplicationModel/Package.Other.cs +++ b/src/Uno.UWP/ApplicationModel/Package.Other.cs @@ -39,7 +39,7 @@ private string GetInstalledPath() public string DisplayName { get; private set; } = ""; - public Uri? Logo { get; set; } = null; + public Uri? Logo { get; set; } internal static void SetEntryAssembly(Assembly entryAssembly) { From 8d92d3e38bbe1c84ea8334e1542592844b1dcb9e Mon Sep 17 00:00:00 2001 From: Martin Zikmund Date: Tue, 25 Jul 2023 15:12:05 +0200 Subject: [PATCH 15/29] feat: Add feature flag for path overriding --- ...nRTFeatureConfiguration.ApplicationData.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/Uno.UWP/WinRTFeatureConfiguration.ApplicationData.cs diff --git a/src/Uno.UWP/WinRTFeatureConfiguration.ApplicationData.cs b/src/Uno.UWP/WinRTFeatureConfiguration.ApplicationData.cs new file mode 100644 index 000000000000..e1f5c9caed1f --- /dev/null +++ b/src/Uno.UWP/WinRTFeatureConfiguration.ApplicationData.cs @@ -0,0 +1,26 @@ +#nullable enable + +namespace Uno; + +partial class WinRTFeatureConfiguration +{ + public static class ApplicationData + { +#if __SKIA__ + /// + /// Allows overriding the root folder path that the application will use + /// to store its TempState folder. + /// + /// Only applies to Skia targets. + public static string? TemporaryFolderPathOverride { get; set; } + + /// + /// Allows overriding the root folder that the application will use + /// to store its application data folders (LocalFolder, RoamingFolder, etc.). + /// Only applies to Skia targets. + /// + /// Only applies to Skia targets. + public static string? ApplicationDataPathOverride { get; set; } +#endif + } +} From d586581fcc110fbbba8ce82f2c33ebe6605bcc31 Mon Sep 17 00:00:00 2001 From: Martin Zikmund Date: Tue, 25 Jul 2023 15:14:30 +0200 Subject: [PATCH 16/29] chore: Adjust application data and temporary folders for Unix-based platforms --- src/Uno.UWP/Storage/ApplicationData.skia.cs | 50 ++++++++++++++++++--- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/src/Uno.UWP/Storage/ApplicationData.skia.cs b/src/Uno.UWP/Storage/ApplicationData.skia.cs index fbdb46bd5c8b..2c9d70091377 100644 --- a/src/Uno.UWP/Storage/ApplicationData.skia.cs +++ b/src/Uno.UWP/Storage/ApplicationData.skia.cs @@ -4,6 +4,8 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; +using System.Runtime.InteropServices; +using Uno; using Windows.ApplicationModel; namespace Windows.Storage; @@ -21,22 +23,22 @@ partial class ApplicationData private static string? _appSpecificSubpath; - private string GetLocalCacheFolder() => EnsurePath(Path.Combine(Path.GetTempPath(), GetAppSpecificSubPath(), LocalCacheFolderName)); + private string GetLocalCacheFolder() => EnsurePath(Path.Combine(GetApplicationDataFolderRootPath(), LocalCacheFolderName)); - private string GetTemporaryFolder() => EnsurePath(Path.Combine(Path.GetTempPath(), GetAppSpecificSubPath(), TemporaryFolderName)); + private string GetTemporaryFolder() => EnsurePath(Path.Combine(GetTemporaryFolderRootPath(), TemporaryFolderName)); private string GetLocalFolder() => // Uses XDG_DATA_HOME on Unix: https://github.com/dotnet/runtime/blob/b5705587347d29d79cec830dc22b389e1ad9a9e0/src/libraries/System.Private.CoreLib/src/System/Environment.GetFolderPathCore.Unix.cs#L105 - EnsurePath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), GetAppSpecificSubPath(), LocalFolderName)); + EnsurePath(Path.Combine(GetApplicationDataFolderRootPath(), LocalFolderName)); private string GetRoamingFolder() => - EnsurePath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), GetAppSpecificSubPath(), RoamingFolderName)); + EnsurePath(Path.Combine(GetApplicationDataFolderRootPath(), RoamingFolderName)); private string GetSharedLocalFolder() => - EnsurePath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), GetAppSpecificSubPath(), SharedLocalFolderName)); + EnsurePath(Path.Combine(GetApplicationDataFolderRootPath(), SharedLocalFolderName)); internal string GetSettingsFolderPath() => - EnsurePath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), GetAppSpecificSubPath(), SettingsFolderName)); + EnsurePath(Path.Combine(GetApplicationDataFolderRootPath(), SettingsFolderName)); private string EnsurePath(string path) { @@ -89,6 +91,42 @@ private string GetAppSpecificSubPath() return _appSpecificSubpath; } + private string GetTemporaryFolderRootPath() + { + if (WinRTFeatureConfiguration.ApplicationData.TemporaryFolderPathOverride is { } path) + { + return path; + } + + return Path.Combine(Path.GetTempPath(), GetAppSpecificSubPath()); + } + + private string GetApplicationDataFolderRootPath() + { + if (WinRTFeatureConfiguration.ApplicationData.ApplicationDataPathOverride is { } path) + { + return path; + } + + string applicationDataRootFolder = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + if (string.IsNullOrEmpty(applicationDataRootFolder)) + { + var myDocumentsFolder = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); + if (string.IsNullOrEmpty(myDocumentsFolder)) + { + throw new InvalidOperationException( + "The current environment does not have a user application data nor user documents folder set up. " + + "Please use WinRTFeatureConfiguration.ApplicationData.ApplicationDataPathOverride to set your own."); + } + else + { + applicationDataRootFolder = Path.Combine(myDocumentsFolder, ".local", "share"); + } + } + + return Path.Combine(applicationDataRootFolder, GetAppSpecificSubPath()); + } + private static string GetFileNameSafeString(string fileName) { foreach (char c in Path.GetInvalidFileNameChars()) From 8db36d5246ce6eb1b43ee15c3044a5ee2dfd8047 Mon Sep 17 00:00:00 2001 From: Martin Zikmund Date: Tue, 25 Jul 2023 15:14:41 +0200 Subject: [PATCH 17/29] test: Assert correct application data paths --- src/SamplesApp/SamplesApp.Shared/App.xaml.cs | 46 ++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/SamplesApp/SamplesApp.Shared/App.xaml.cs b/src/SamplesApp/SamplesApp.Shared/App.xaml.cs index 2375dc220b5f..5e925db6725d 100644 --- a/src/SamplesApp/SamplesApp.Shared/App.xaml.cs +++ b/src/SamplesApp/SamplesApp.Shared/App.xaml.cs @@ -35,6 +35,7 @@ using Uno.UI.Xaml.Controls.Extensions; using Uno.Foundation.Extensibility; using MUXControlsTestApp.Utilities; +using Windows.Storage; #endif #if !HAS_UNO @@ -90,6 +91,7 @@ public App() ConfigureFeatureFlags(); AssertIssue1790ApplicationSettingsUsable(); + AssertApplicationDataFolders(); this.InitializeComponent(); this.Suspending += OnSuspending; @@ -673,6 +675,50 @@ public void AssertIssue8641NativeOverlayInitialized() var textBoxView = new TextBoxView(textBox); ApiExtensibility.CreateInstance(textBoxView, out var textBoxViewExtension); Assert.IsTrue(textBoxViewExtension.IsOverlayLayerInitialized(rootFrame.XamlRoot)); +#endif + } + + public void AssertApplicationDataFolders() + { +#if __SKIA__ + var appName = Package.Current.Id.Name; + var publisher = string.IsNullOrEmpty(Package.Current.Id.Publisher) ? "" : "Uno Platform"; + + AssertForFolder(ApplicationData.Current.LocalFolder); + AssertForFolder(ApplicationData.Current.RoamingFolder); + AssertForFolder(ApplicationData.Current.TemporaryFolder); + AssertForFolder(ApplicationData.Current.SharedLocalFolder); + AssertForFolder(ApplicationData.Current.LocalCacheFolder); + + void AssertForFolder(StorageFolder folder) + { + AssertContainsIdProps(folder); + AssertCanCreateFile(folder); + } + + void AssertContainsIdProps(StorageFolder folder) + { + Assert.IsTrue(folder.Path.Contains(appName, StringComparison.Ordinal)); + Assert.IsTrue(folder.Path.Contains(publisher, StringComparison.Ordinal)); + } + + void AssertCanCreateFile(StorageFolder folder) + { + var filename = Guid.NewGuid() + ".txt"; + var path = Path.Combine(folder.Path, filename); + var expectedContent = "Test"; + try + { + File.WriteAllText(path, expectedContent); + var actualContent = File.ReadAllText(path); + + Assert.AreEqual(expectedContent, actualContent); + } + finally + { + File.Delete(path); + } + } #endif } } From 8eb74828455cb6241e50de37a78f01082108a0c9 Mon Sep 17 00:00:00 2001 From: Martin Zikmund Date: Tue, 25 Jul 2023 15:26:47 +0200 Subject: [PATCH 18/29] chore: Adjust naming of files --- .../{PackageId.netstd.cs => PackageId.crossruntime.cs} | 0 src/Uno.UWP/ApplicationModel/PackageId.defaults.cs | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/Uno.UWP/ApplicationModel/{PackageId.netstd.cs => PackageId.crossruntime.cs} (100%) diff --git a/src/Uno.UWP/ApplicationModel/PackageId.netstd.cs b/src/Uno.UWP/ApplicationModel/PackageId.crossruntime.cs similarity index 100% rename from src/Uno.UWP/ApplicationModel/PackageId.netstd.cs rename to src/Uno.UWP/ApplicationModel/PackageId.crossruntime.cs diff --git a/src/Uno.UWP/ApplicationModel/PackageId.defaults.cs b/src/Uno.UWP/ApplicationModel/PackageId.defaults.cs index f84bbaaa776a..528f9e11390f 100644 --- a/src/Uno.UWP/ApplicationModel/PackageId.defaults.cs +++ b/src/Uno.UWP/ApplicationModel/PackageId.defaults.cs @@ -37,7 +37,7 @@ partial class PackageId public PackageVersion Version { get; internal set; } = new PackageVersion(Assembly.GetExecutingAssembly().GetVersionNumber()); #endif -#if !__WASM__ && !__SKIA__ && !__NETSTD_REFERENCE__ +#if !__WASM__ && !__SKIA__ && !__CROSSRUNTIME__ [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__MACOS__")] public string Publisher { get; internal set; } = "Unknown"; #endif From e59e24e45f7ff1d40d29851f4201f3aeeb4bdedd Mon Sep 17 00:00:00 2001 From: Martin Zikmund Date: Wed, 26 Jul 2023 13:28:59 +0200 Subject: [PATCH 19/29] perf: Initialize ApplicationData folders lazily --- src/SamplesApp/SamplesApp.Shared/App.xaml.cs | 3 +- src/Uno.UWP/Storage/ApplicationData.cs | 139 ++++++++++++------- 2 files changed, 91 insertions(+), 51 deletions(-) diff --git a/src/SamplesApp/SamplesApp.Shared/App.xaml.cs b/src/SamplesApp/SamplesApp.Shared/App.xaml.cs index 5e925db6725d..0e3965e752a8 100644 --- a/src/SamplesApp/SamplesApp.Shared/App.xaml.cs +++ b/src/SamplesApp/SamplesApp.Shared/App.xaml.cs @@ -686,8 +686,7 @@ public void AssertApplicationDataFolders() AssertForFolder(ApplicationData.Current.LocalFolder); AssertForFolder(ApplicationData.Current.RoamingFolder); - AssertForFolder(ApplicationData.Current.TemporaryFolder); - AssertForFolder(ApplicationData.Current.SharedLocalFolder); + AssertForFolder(ApplicationData.Current.TemporaryFolder); AssertForFolder(ApplicationData.Current.LocalCacheFolder); void AssertForFolder(StorageFolder folder) diff --git a/src/Uno.UWP/Storage/ApplicationData.cs b/src/Uno.UWP/Storage/ApplicationData.cs index 6173deb47616..b926141187e9 100644 --- a/src/Uno.UWP/Storage/ApplicationData.cs +++ b/src/Uno.UWP/Storage/ApplicationData.cs @@ -1,77 +1,118 @@ +#nullable enable + #if !IS_UNIT_TESTS #pragma warning disable 108 // new keyword hiding #pragma warning disable 114 // new keyword hiding #pragma warning disable 67 // new keyword hiding using System; -namespace Windows.Storage +namespace Windows.Storage; + +/// +/// Provides access to the application data store. Application data consists of files +/// and settings that are either local, roaming, or temporary. +/// +public sealed partial class ApplicationData { - public sealed partial class ApplicationData + private readonly Lazy _localFolderLazy; + private readonly Lazy _roamingFolderLazy; + private readonly Lazy _sharedLocalFolderLazy; + private readonly Lazy _localCacheFolderLazy; + private readonly Lazy _temporaryFolderLazy; + private readonly Lazy _localSettingsLazy; + private readonly Lazy _roamingSettingsLazy; + + private ApplicationData() { - public static ApplicationData Current { get; } = new ApplicationData(); - - private ApplicationData() - { - LocalFolder = new StorageFolder(GetLocalFolder()); - RoamingFolder = new StorageFolder(GetRoamingFolder()); - SharedLocalFolder = new StorageFolder(".shared", GetSharedLocalFolder()); - LocalCacheFolder = new StorageFolder(GetLocalCacheFolder()); - TemporaryFolder = new StorageFolder(GetTemporaryFolder()); + _localFolderLazy = new(() => new StorageFolder(GetLocalFolder())); + _roamingFolderLazy = new(() => new StorageFolder(GetRoamingFolder())); +#if !__SKIA__ // The concept of Shared Local folder is not implemented for Skia. + _sharedLocalFolderLazy = new StorageFolder(".shared", GetSharedLocalFolder()); +#else + _sharedLocalFolderLazy = new((StorageFolder?)null); +#endif + _localCacheFolderLazy = new(() => new StorageFolder(GetLocalCacheFolder())); + _temporaryFolderLazy = new(() => new StorageFolder(GetTemporaryFolder())); - LocalSettings = new ApplicationDataContainer(this, "Local", ApplicationDataLocality.Local); - RoamingSettings = new ApplicationDataContainer(this, "Roaming", ApplicationDataLocality.Roaming); + _localSettingsLazy = new(() => new ApplicationDataContainer(this, "Local", ApplicationDataLocality.Local)); + _roamingSettingsLazy = new(() => new ApplicationDataContainer(this, "Roaming", ApplicationDataLocality.Roaming)); - InitializePartial(); - } + InitializePartial(); + } - partial void InitializePartial(); + partial void InitializePartial(); - public StorageFolder LocalFolder { get; } + /// + /// Provides access to the app data store associated with the app's app package. + /// + public static ApplicationData Current { get; } = new(); - public StorageFolder RoamingFolder { get; } + /// + /// Gets the root folder in the local app data store. This folder is backed up to the cloud. + /// + public StorageFolder LocalFolder => _localFolderLazy.Value; - public StorageFolder SharedLocalFolder { get; } + /// + /// Gets the root folder in the roaming app data store. + /// + public StorageFolder RoamingFolder => _roamingFolderLazy.Value; - public StorageFolder LocalCacheFolder { get; } + /// + /// Gets the root folder in the shared app data store. + /// + public StorageFolder? SharedLocalFolder => _sharedLocalFolderLazy.Value; - public StorageFolder TemporaryFolder { get; } + /// + /// Gets the folder in the local app data store where you can save files that are not included in backup and restore. + /// + public StorageFolder LocalCacheFolder => _localCacheFolderLazy.Value; - public ApplicationDataContainer LocalSettings { get; } + /// + /// Gets the root folder in the temporary app data store. + /// + public StorageFolder TemporaryFolder => _temporaryFolderLazy.Value; - public ApplicationDataContainer RoamingSettings { get; } + /// + /// Gets the application settings container in the local app data store. + /// + public ApplicationDataContainer LocalSettings => _localSettingsLazy.Value; - [Uno.NotImplemented] - public ulong RoamingStorageQuota => 0; + /// + /// Gets the root folder in the roaming app data store. + /// + public ApplicationDataContainer RoamingSettings => _roamingSettingsLazy.Value; - [Uno.NotImplemented] - public uint Version => 0; + [Uno.NotImplemented] + public ulong RoamingStorageQuota => 0; - [Uno.NotImplemented] - public void SignalDataChanged() - { - Foundation.Metadata.ApiInformation.TryRaiseNotImplemented("Windows.Storage.ApplicationData", "void ApplicationData.SignalDataChanged()"); - } + [Uno.NotImplemented] + public uint Version => 0; - [Uno.NotImplemented] - public StorageFolder GetPublisherCacheFolder(string folderName) - { - throw new NotImplementedException("The member StorageFolder ApplicationData.GetPublisherCacheFolder(string folderName) is not implemented in Uno."); - } + [Uno.NotImplemented] + public void SignalDataChanged() + { + Foundation.Metadata.ApiInformation.TryRaiseNotImplemented("Windows.Storage.ApplicationData", "void ApplicationData.SignalDataChanged()"); + } - [Uno.NotImplemented] - public Foundation.IAsyncAction ClearPublisherCacheFolderAsync(string folderName) - { - throw new NotImplementedException("The member IAsyncAction ApplicationData.ClearPublisherCacheFolderAsync(string folderName) is not implemented in Uno."); - } + [Uno.NotImplemented] + public StorageFolder GetPublisherCacheFolder(string folderName) + { + throw new NotImplementedException("The member StorageFolder ApplicationData.GetPublisherCacheFolder(string folderName) is not implemented in Uno."); + } - [Uno.NotImplemented] - public static Foundation.IAsyncOperation GetForUserAsync(System.User user) - { - throw new NotImplementedException("The member IAsyncOperation ApplicationData.GetForUserAsync(User user) is not implemented in Uno."); - } + [Uno.NotImplemented] + public Foundation.IAsyncAction ClearPublisherCacheFolderAsync(string folderName) + { + throw new NotImplementedException("The member IAsyncAction ApplicationData.ClearPublisherCacheFolderAsync(string folderName) is not implemented in Uno."); + } - [Uno.NotImplemented] - public event Foundation.TypedEventHandler DataChanged; + [Uno.NotImplemented] + public static Foundation.IAsyncOperation GetForUserAsync(System.User user) + { + throw new NotImplementedException("The member IAsyncOperation ApplicationData.GetForUserAsync(User user) is not implemented in Uno."); } + + [Uno.NotImplemented] + public event Foundation.TypedEventHandler? DataChanged; } #endif From 500a46079dedbc9ed776726949578ed0f760f727 Mon Sep 17 00:00:00 2001 From: Martin Zikmund Date: Wed, 26 Jul 2023 13:29:25 +0200 Subject: [PATCH 20/29] feat: Allow LocalCache folder path override --- .../Storage/ApplicationData.reference.cs | 17 +++-- src/Uno.UWP/Storage/ApplicationData.skia.cs | 63 ++++++++++++++----- ...nRTFeatureConfiguration.ApplicationData.cs | 7 +++ 3 files changed, 61 insertions(+), 26 deletions(-) diff --git a/src/Uno.UWP/Storage/ApplicationData.reference.cs b/src/Uno.UWP/Storage/ApplicationData.reference.cs index c48ae18581ec..c69215d43a24 100644 --- a/src/Uno.UWP/Storage/ApplicationData.reference.cs +++ b/src/Uno.UWP/Storage/ApplicationData.reference.cs @@ -1,3 +1,5 @@ +#nullable enable + using System; using System.IO; @@ -5,18 +7,13 @@ namespace Windows.Storage; partial class ApplicationData { - private static string GetLocalCacheFolder() - => Path.GetTempPath(); + private static string GetLocalCacheFolder() => ""; - private static string GetTemporaryFolder() - => Path.GetTempPath(); + private static string GetTemporaryFolder() => ""; - private static string GetLocalFolder() - => AppDomain.CurrentDomain.BaseDirectory; + private static string GetLocalFolder() => ""; - private static string GetRoamingFolder() - => Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + private static string GetRoamingFolder() => ""; - private static string GetSharedLocalFolder() - => Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + private static string GetSharedLocalFolder() => ""; } diff --git a/src/Uno.UWP/Storage/ApplicationData.skia.cs b/src/Uno.UWP/Storage/ApplicationData.skia.cs index 2c9d70091377..6192fb405b7c 100644 --- a/src/Uno.UWP/Storage/ApplicationData.skia.cs +++ b/src/Uno.UWP/Storage/ApplicationData.skia.cs @@ -23,7 +23,7 @@ partial class ApplicationData private static string? _appSpecificSubpath; - private string GetLocalCacheFolder() => EnsurePath(Path.Combine(GetApplicationDataFolderRootPath(), LocalCacheFolderName)); + private string GetLocalCacheFolder() => EnsurePath(Path.Combine(GetLocalCacheFolderRootPath(), LocalCacheFolderName)); private string GetTemporaryFolder() => EnsurePath(Path.Combine(GetTemporaryFolderRootPath(), TemporaryFolderName)); @@ -34,9 +34,6 @@ private string GetLocalFolder() => private string GetRoamingFolder() => EnsurePath(Path.Combine(GetApplicationDataFolderRootPath(), RoamingFolderName)); - private string GetSharedLocalFolder() => - EnsurePath(Path.Combine(GetApplicationDataFolderRootPath(), SharedLocalFolderName)); - internal string GetSettingsFolderPath() => EnsurePath(Path.Combine(GetApplicationDataFolderRootPath(), SettingsFolderName)); @@ -101,6 +98,42 @@ private string GetTemporaryFolderRootPath() return Path.Combine(Path.GetTempPath(), GetAppSpecificSubPath()); } + private string GetLocalCacheFolderRootPath() + { + if (WinRTFeatureConfiguration.ApplicationData.LocalCacheFolderPathOverride is { } path) + { + return path; + } + + string? localCacheRootFolder = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? + Environment.GetEnvironmentVariable("XDG_CACHE_HOME") : + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + + if (string.IsNullOrEmpty(localCacheRootFolder)) + { + var userHomeFolderPath = GetUserHomeFolderPath(); + + localCacheRootFolder = Path.Combine(userHomeFolderPath, ".cache"); + } + + return Path.Combine(localCacheRootFolder, GetAppSpecificSubPath()); + } + + private static string GetUserHomeFolderPath() + { + var myDocumentsFolder = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); + + if (string.IsNullOrEmpty(myDocumentsFolder)) + { + throw new InvalidOperationException( + "The current environment does not have a user application data nor user documents folder set up. " + + "Please use WinRTFeatureConfiguration.ApplicationData.ApplicationDataPathOverride and " + + "WinRTFeatureConfiguration.ApplicationData.LocalCacheFolderPathOverride to override the default locations."); + } + + return myDocumentsFolder; + } + private string GetApplicationDataFolderRootPath() { if (WinRTFeatureConfiguration.ApplicationData.ApplicationDataPathOverride is { } path) @@ -108,20 +141,18 @@ private string GetApplicationDataFolderRootPath() return path; } - string applicationDataRootFolder = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + string? applicationDataRootFolder = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + if (string.IsNullOrEmpty(applicationDataRootFolder)) { - var myDocumentsFolder = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); - if (string.IsNullOrEmpty(myDocumentsFolder)) - { - throw new InvalidOperationException( - "The current environment does not have a user application data nor user documents folder set up. " + - "Please use WinRTFeatureConfiguration.ApplicationData.ApplicationDataPathOverride to set your own."); - } - else - { - applicationDataRootFolder = Path.Combine(myDocumentsFolder, ".local", "share"); - } + applicationDataRootFolder = Environment.GetEnvironmentVariable("XDG_DATA_HOME"); + } + + if (string.IsNullOrEmpty(applicationDataRootFolder)) + { + var userHomeFolderPath = GetUserHomeFolderPath(); + + applicationDataRootFolder = Path.Combine(userHomeFolderPath, ".local", "share"); } return Path.Combine(applicationDataRootFolder, GetAppSpecificSubPath()); diff --git a/src/Uno.UWP/WinRTFeatureConfiguration.ApplicationData.cs b/src/Uno.UWP/WinRTFeatureConfiguration.ApplicationData.cs index e1f5c9caed1f..d99c6d4007f6 100644 --- a/src/Uno.UWP/WinRTFeatureConfiguration.ApplicationData.cs +++ b/src/Uno.UWP/WinRTFeatureConfiguration.ApplicationData.cs @@ -14,6 +14,13 @@ public static class ApplicationData /// Only applies to Skia targets. public static string? TemporaryFolderPathOverride { get; set; } + /// + /// Allows overriding the root folder path that the application will use + /// to store its LocalCache folder. + /// + /// Only applies to Skia targets. + public static string? LocalCacheFolderPathOverride { get; set; } + /// /// Allows overriding the root folder that the application will use /// to store its application data folders (LocalFolder, RoamingFolder, etc.). From dbe87098b5bb5d48732e90a9073c2cc2cf954c26 Mon Sep 17 00:00:00 2001 From: Martin Zikmund Date: Wed, 26 Jul 2023 15:24:37 +0200 Subject: [PATCH 21/29] chore: Adjust setter --- src/Uno.UWP/Storage/ApplicationData.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Uno.UWP/Storage/ApplicationData.cs b/src/Uno.UWP/Storage/ApplicationData.cs index b926141187e9..962fa4258016 100644 --- a/src/Uno.UWP/Storage/ApplicationData.cs +++ b/src/Uno.UWP/Storage/ApplicationData.cs @@ -27,7 +27,7 @@ private ApplicationData() _localFolderLazy = new(() => new StorageFolder(GetLocalFolder())); _roamingFolderLazy = new(() => new StorageFolder(GetRoamingFolder())); #if !__SKIA__ // The concept of Shared Local folder is not implemented for Skia. - _sharedLocalFolderLazy = new StorageFolder(".shared", GetSharedLocalFolder()); + _sharedLocalFolderLazy = new(() => new StorageFolder(".shared", GetSharedLocalFolder())); #else _sharedLocalFolderLazy = new((StorageFolder?)null); #endif From 722e12413872bb1dc7e6ab86c710b91241674836 Mon Sep 17 00:00:00 2001 From: Martin Zikmund Date: Wed, 26 Jul 2023 16:02:12 +0200 Subject: [PATCH 22/29] chore: Use UserProfile folder --- src/Uno.UWP/Storage/ApplicationData.skia.cs | 30 ++++++++++----------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Uno.UWP/Storage/ApplicationData.skia.cs b/src/Uno.UWP/Storage/ApplicationData.skia.cs index 6192fb405b7c..66b415971d41 100644 --- a/src/Uno.UWP/Storage/ApplicationData.skia.cs +++ b/src/Uno.UWP/Storage/ApplicationData.skia.cs @@ -119,21 +119,6 @@ private string GetLocalCacheFolderRootPath() return Path.Combine(localCacheRootFolder, GetAppSpecificSubPath()); } - private static string GetUserHomeFolderPath() - { - var myDocumentsFolder = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); - - if (string.IsNullOrEmpty(myDocumentsFolder)) - { - throw new InvalidOperationException( - "The current environment does not have a user application data nor user documents folder set up. " + - "Please use WinRTFeatureConfiguration.ApplicationData.ApplicationDataPathOverride and " + - "WinRTFeatureConfiguration.ApplicationData.LocalCacheFolderPathOverride to override the default locations."); - } - - return myDocumentsFolder; - } - private string GetApplicationDataFolderRootPath() { if (WinRTFeatureConfiguration.ApplicationData.ApplicationDataPathOverride is { } path) @@ -158,6 +143,21 @@ private string GetApplicationDataFolderRootPath() return Path.Combine(applicationDataRootFolder, GetAppSpecificSubPath()); } + private static string GetUserHomeFolderPath() + { + var myDocumentsFolder = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + + if (string.IsNullOrEmpty(myDocumentsFolder)) + { + throw new InvalidOperationException( + "The current environment does not have a user application data nor user documents folder set up. " + + "Please use WinRTFeatureConfiguration.ApplicationData.ApplicationDataPathOverride and " + + "WinRTFeatureConfiguration.ApplicationData.LocalCacheFolderPathOverride to override the default locations."); + } + + return myDocumentsFolder; + } + private static string GetFileNameSafeString(string fileName) { foreach (char c in Path.GetInvalidFileNameChars()) From c6ce2a75c5f212926196994700d58f071616a86e Mon Sep 17 00:00:00 2001 From: Martin Zikmund Date: Thu, 27 Jul 2023 09:03:36 +0200 Subject: [PATCH 23/29] chore: Remove SharedLocalFolder support --- src/Uno.UWP/Storage/ApplicationData.skia.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Uno.UWP/Storage/ApplicationData.skia.cs b/src/Uno.UWP/Storage/ApplicationData.skia.cs index 66b415971d41..08f1b7ee6120 100644 --- a/src/Uno.UWP/Storage/ApplicationData.skia.cs +++ b/src/Uno.UWP/Storage/ApplicationData.skia.cs @@ -17,7 +17,6 @@ partial class ApplicationData private const string LocalCacheFolderName = "LocalCache"; private const string TemporaryFolderName = "TempState"; private const string LocalFolderName = "LocalState"; - private const string SharedLocalFolderName = "SharedLocalState"; private const string RoamingFolderName = "RoamingState"; private const string SettingsFolderName = "Settings"; From 6ce2f40238e449741bbc3a6159b63dff2151d386 Mon Sep 17 00:00:00 2001 From: Martin Zikmund Date: Thu, 27 Jul 2023 11:46:09 +0200 Subject: [PATCH 24/29] test: Assert settings can be used immediately after app initialization on Skia --- src/SamplesApp/SamplesApp.Shared/App.xaml.cs | 23 +++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/SamplesApp/SamplesApp.Shared/App.xaml.cs b/src/SamplesApp/SamplesApp.Shared/App.xaml.cs index 0e3965e752a8..3012101a5168 100644 --- a/src/SamplesApp/SamplesApp.Shared/App.xaml.cs +++ b/src/SamplesApp/SamplesApp.Shared/App.xaml.cs @@ -91,7 +91,7 @@ public App() ConfigureFeatureFlags(); AssertIssue1790ApplicationSettingsUsable(); - AssertApplicationDataFolders(); + AssertApplicationData(); this.InitializeComponent(); this.Suspending += OnSuspending; @@ -678,7 +678,11 @@ public void AssertIssue8641NativeOverlayInitialized() #endif } - public void AssertApplicationDataFolders() + /// + /// Verifies that ApplicationData are available immediately after the application class is created + /// and the data are stored in proper application specific lcoations. + /// + public void AssertApplicationData() { #if __SKIA__ var appName = Package.Current.Id.Name; @@ -686,8 +690,10 @@ public void AssertApplicationDataFolders() AssertForFolder(ApplicationData.Current.LocalFolder); AssertForFolder(ApplicationData.Current.RoamingFolder); - AssertForFolder(ApplicationData.Current.TemporaryFolder); + AssertForFolder(ApplicationData.Current.TemporaryFolder); AssertForFolder(ApplicationData.Current.LocalCacheFolder); + AssertSettings(ApplicationData.Current.LocalSettings); + AssertSettings(ApplicationData.Current.RoamingSettings); void AssertForFolder(StorageFolder folder) { @@ -695,6 +701,17 @@ void AssertForFolder(StorageFolder folder) AssertCanCreateFile(folder); } + void AssertSettings(ApplicationDataContainer container) + { + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + + container.Values[key] = value; + Assert.IsTrue(container.Values.ContainsKey(key)); + Assert.AreEqual(value, container.Values[key]); + container.Values.Remove(key); + } + void AssertContainsIdProps(StorageFolder folder) { Assert.IsTrue(folder.Path.Contains(appName, StringComparison.Ordinal)); From 6812c00f0049ba3b0f78f1d488ad940181b9c1da Mon Sep 17 00:00:00 2001 From: Martin Zikmund Date: Thu, 27 Jul 2023 11:46:57 +0200 Subject: [PATCH 25/29] docs: Application Data and Settings --- .../features/applicationdata/appdata.jpeg | Bin 0 -> 75900 bytes doc/articles/features/applicationdata.md | 86 ++++++++++++++++++ doc/articles/features/file-management.md | 4 +- .../migrating-from-previous-releases.md | 3 + doc/articles/toc.yml | 2 + 5 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 doc/articles/Assets/features/applicationdata/appdata.jpeg create mode 100644 doc/articles/features/applicationdata.md diff --git a/doc/articles/Assets/features/applicationdata/appdata.jpeg b/doc/articles/Assets/features/applicationdata/appdata.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..9bcc4c0efa23bb1c0fa25595e0ce3ee185ba6ee6 GIT binary patch literal 75900 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 literal 0 HcmV?d00001 diff --git a/doc/articles/features/applicationdata.md b/doc/articles/features/applicationdata.md new file mode 100644 index 000000000000..488cc09bd7d9 --- /dev/null +++ b/doc/articles/features/applicationdata.md @@ -0,0 +1,86 @@ +--- +uid: Uno.Features.ApplicationData +--- + +# Application Data and Settings + +![Application Data and Preferences](../Assets/features/applicationdata/appdata.jpeg) + +To store persistent application-specifc data and user preferences, you can utilize the `Windows.Storage.ApplicationData` class in Uno Platform. + +Legend + - ✔️ Supported + +| Picker | WinUI/UWP | WebAssembly | Android | iOS/Mac Catalyst | macOS | WPF | GTK | +|----------------|-------|-------------|---------|-------|-------|-----|-----| +| `LocalFolder` | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | +| `RoamingFolder` | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | +| `LocalCacheFolder` | ✔️ | ✔️ | ✔️ | ✔️| ✔️ | ✔️ | ✔️ | +| `TemporaryFolder` | ✔️ | ✔️ | ✔️ | ✔️| ✔️ | ✔️ | ✔️ | +| `LocalSettings` | ✔️ | ✔️ | ✔️ | ✔️| ✔️ | ✔️ | ✔️ | +| `RoamingSettings` | ✔️ | ✔️ | ✔️ | ✔️| ✔️ | ✔️ | ✔️ | + +Please note that `RoamingFolder` and `RoamingSettings` are not roamed automatically across devices, they only provide a logical separation between data that you intend to roam and that you intend to keep local. + +## Storing application data + +There are several folders where persistent application data can be stored: + +- `LocalFolder/RoamingFolder` - general-use application files +- `TemporaryFolder` - files with limited lifetime +- `LocalCacheFolder` - cached files retrieved from external services + +In the case of `TemporaryFolder` and `LocalCacheFolder` it is crucial to remember that the user or operating system may purge files stored in these locations to reclaim storage space. To store persistent files prefer the `LocalFolder` or `RoamingFolder`. + +The following example shows how you can create a file in `LocalFolder` and then read the contents back: + +```csharp +StorageFolder folder = ApplicationData.Current.LocalFolder; + +// Create a file in the root of LocalFolder. +StorageFile file = await folder.CreateFileAsync("file.txt", CreationCollisionOption.ReplaceExisting); + +// Write text in the newly created file. +await FileIO.WriteTextAsync(file, "Hello, Uno Platform!"); + +// Read the text from file. +string text = await FileIO.ReadTextAsync(file); +``` + +## Storing settings + +The `LocalSettings` and `RoamingSettings` properties provide access to simple key-value containers that allow storage of lightweight user and application preferences. The values stored in settings should be simple serializable types. To store more complex data structures, it is preferred to serialize them first into a string (for example using a JSON serializer). + +``` csharp +ApplicationDataContainer localSettings = ApplicationData.Current.LocalSettings; + +// Save a setting. +localSettings.Values["name"] = "Martin"; + +// Read a setting. +string value = (string)localSettings.Values["name"]; +``` + +## Data location on GTK and WPF + +In case of GTK and WPF targets the data are stored in application- and user-specific locations on the hard drive. The default path to the various folders dependes on the runtime operating system: + +**Windows** + +- `LocalFolder` - `C:\Users\UserName>\AppData\Local\\\LocalState` +- `RoamingFolder` - `C:\Users\\AppData\Local\\\RoamingState` +- `LocalCahe` - `C:\Users\\AppData\Local\\\LocalCache` +- `TemporaryFolder` - `C:\Users\\AppData\Local\\\RoamingState` +- `LocalSettings` - `C:\Users\\AppData\Local\Temp\\\TempState` +- `RoamingSettings` - `C:\Users\\AppData\Local\\\Settings\Roaming.dat` + +**Unix-based systems** + +- `LocalFolder` - `/home//.local/share///LocalState` +- `RoamingFolder` - `/home//.local/share///RoamingState` +- `TemporaryFolder` - `/tmp///TempState` +- `LocalCache` - `/home//.cache///LocalCache` +- `LocalSettings` - `/home//.local/share///Settings/Local.dat` +- `RoamingSettings` - `/home//.local/share///Settings/Roaming.dat` + +Where `` is the name of the currently logged-in user and `` and `ApplicationName` are values coming from `Package.appxmanifest` (note that the publisher value is prefixed by `CN=` in the manifest, but this is excluded from the folder name). \ No newline at end of file diff --git a/doc/articles/features/file-management.md b/doc/articles/features/file-management.md index bc06ba72bc2a..9179b10977ee 100644 --- a/doc/articles/features/file-management.md +++ b/doc/articles/features/file-management.md @@ -11,12 +11,10 @@ uid: Uno.Features.FileManagement ## Supported features -| Feature | Windows | Android | iOS | Web (WASM) | macOS | Linux (Skia) | Win 7 (Skia) | +| Feature | WinUI/UWP | Android | iOS | Web (WASM) | macOS | Linux (Skia) | WPF (Skia) | |---------------|-------|-------|-------|-------|-------|-------|-| | `StorageFile` | ✔ | ✔ | ✔| ✔ | ✔| ✔ |✔ | | `StorageFolder` | ✔ | ✔ | ✔| ✔ | ✔| ✔ |✔ | -| `ApplicationData.Current.LocalFolder` | ✔ | ✔ | ✔| ✔ | ✔| ✔ |✔ | -| `ApplicationData.Current.RoamingFolder` | ✔ | ✔ | ✔| ✔ | ✔| ✔ |✔ | | `CachedFileManager` | ✔ | partial | partial | partial | partial | partial | partial | | `StorageFileHelper` | ✔ | ✔ | ✔| ✔ | ✔| ✔ |✔ | diff --git a/doc/articles/migrating-from-previous-releases.md b/doc/articles/migrating-from-previous-releases.md index b3a7f83563eb..3aaf0744b3d6 100644 --- a/doc/articles/migrating-from-previous-releases.md +++ b/doc/articles/migrating-from-previous-releases.md @@ -21,6 +21,9 @@ Uno Platform 5.0 continues to supports both UWP and WinUI API sets. #### Migrating from Xamarin to net7.0-* targets If your current project is built on Xamarin.* targets, you can upgrade by [following this guide](xref:Uno.Development.MigratingFromXamarinToNet6). +#### Migrating `ApplicationData` on Skia targets +Previously, `ApplicationData` were stored directly in `Environment.SpecialFolder.LocalApplicationData` folder, and all Uno Platform apps shared this single location. Starting with Uno Platform 5.0, application data are stored in application specific subfolders under the `LocalApplicationData` root. For more details see the [docs](features/applicationdata.md). To perform the initial migration of existing data you need to make sure to copy the files from the root of the `LocalApplicationData` folder to `ApplicationData.Current.LocalFolder` manually using `System.IO`. + #### `ShouldWriteErrorOnInvalidXaml` now defaults to true. Invalid XAML, such as unknown properties or unknown x:Bind targets will generate a compiler error. Those errors must now be fixed as they are no longer ignored. diff --git a/doc/articles/toc.yml b/doc/articles/toc.yml index fd92c1acc198..dd0738f92f03 100644 --- a/doc/articles/toc.yml +++ b/doc/articles/toc.yml @@ -243,6 +243,8 @@ href: features/app-close-handler.md - name: App Suspension href: features/windows-ui-xaml-application.md + - name: Application Data and Settings + href: features/applicationdata.md - name: Badge Notifications href: features/windows-ui-notifications.md - name: Barometer From f357b94bd341fe930705b8cff8f1b7e305e073cc Mon Sep 17 00:00:00 2001 From: Martin Zikmund Date: Thu, 27 Jul 2023 11:59:37 +0200 Subject: [PATCH 26/29] chore: Adjust docs --- doc/articles/features/applicationdata.md | 6 +++--- doc/articles/migrating-from-previous-releases.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/articles/features/applicationdata.md b/doc/articles/features/applicationdata.md index 488cc09bd7d9..e52113b8ebc7 100644 --- a/doc/articles/features/applicationdata.md +++ b/doc/articles/features/applicationdata.md @@ -6,7 +6,7 @@ uid: Uno.Features.ApplicationData ![Application Data and Preferences](../Assets/features/applicationdata/appdata.jpeg) -To store persistent application-specifc data and user preferences, you can utilize the `Windows.Storage.ApplicationData` class in Uno Platform. +To store persistent application data and user settings, you can utilize the `Windows.Storage.ApplicationData` class in Uno Platform. Legend - ✔️ Supported @@ -63,7 +63,7 @@ string value = (string)localSettings.Values["name"]; ## Data location on GTK and WPF -In case of GTK and WPF targets the data are stored in application- and user-specific locations on the hard drive. The default path to the various folders dependes on the runtime operating system: +In case of GTK and WPF targets the data are stored in application- and user-specific locations on the hard drive. The default path to the various folders depends on the runtime operating system: **Windows** @@ -83,4 +83,4 @@ In case of GTK and WPF targets the data are stored in application- and user-spec - `LocalSettings` - `/home//.local/share///Settings/Local.dat` - `RoamingSettings` - `/home//.local/share///Settings/Roaming.dat` -Where `` is the name of the currently logged-in user and `` and `ApplicationName` are values coming from `Package.appxmanifest` (note that the publisher value is prefixed by `CN=` in the manifest, but this is excluded from the folder name). \ No newline at end of file +Where `` is the name of the currently logged-in user and `` and `` are values coming from the `` node of the `Package.appxmanifest` (note that the publisher value is prefixed by `CN=` in the manifest, but this is excluded from the folder name). \ No newline at end of file diff --git a/doc/articles/migrating-from-previous-releases.md b/doc/articles/migrating-from-previous-releases.md index 3aaf0744b3d6..ac269617c214 100644 --- a/doc/articles/migrating-from-previous-releases.md +++ b/doc/articles/migrating-from-previous-releases.md @@ -22,7 +22,7 @@ Uno Platform 5.0 continues to supports both UWP and WinUI API sets. If your current project is built on Xamarin.* targets, you can upgrade by [following this guide](xref:Uno.Development.MigratingFromXamarinToNet6). #### Migrating `ApplicationData` on Skia targets -Previously, `ApplicationData` were stored directly in `Environment.SpecialFolder.LocalApplicationData` folder, and all Uno Platform apps shared this single location. Starting with Uno Platform 5.0, application data are stored in application specific subfolders under the `LocalApplicationData` root. For more details see the [docs](features/applicationdata.md). To perform the initial migration of existing data you need to make sure to copy the files from the root of the `LocalApplicationData` folder to `ApplicationData.Current.LocalFolder` manually using `System.IO`. +Previously, `ApplicationData` were stored directly in `Environment.SpecialFolder.LocalApplicationData` folder, and all Uno Platform apps shared this single location. Starting with Uno Platform 5.0, application data are stored in application specific folders under the `LocalApplicationData` root. For more details see the [docs](features/applicationdata.md). To perform the initial migration of existing data you need to make sure to copy the files from the root of the `LocalApplicationData` folder to `ApplicationData.Current.LocalFolder` manually using `System.IO`. #### `ShouldWriteErrorOnInvalidXaml` now defaults to true. Invalid XAML, such as unknown properties or unknown x:Bind targets will generate a compiler error. Those errors must now be fixed as they are no longer ignored. From cb19c32827ad055b5e8cea81b77c7674e9f0ab13 Mon Sep 17 00:00:00 2001 From: Martin Zikmund Date: Thu, 27 Jul 2023 14:27:26 +0200 Subject: [PATCH 27/29] docs: Add documentation for ApplicationData feature flags --- doc/articles/feature-flags.md | 4 ++++ doc/articles/features/applicationdata.md | 19 +++++++++++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/doc/articles/feature-flags.md b/doc/articles/feature-flags.md index 0dc89a5f7e2e..f87196c7d19a 100644 --- a/doc/articles/feature-flags.md +++ b/doc/articles/feature-flags.md @@ -98,3 +98,7 @@ Uno.UI.FeatureConfiguration.ToolTip.UseToolTips = true; ``` It is also possible to adjust the delay in milliseconds (`Uno.UI.FeatureConfiguration.ToolTip.ShowDelay` - defaults to `1000`) and show duration in milliseconds (`Uno.UI.FeatureConfiguration.ToolTip.ShowDuration` - defaults to `5000`). This configuration only applies to Uno Platform targets. Windows App SDK/UWP will not adhere to this configuration. + +## `ApplicationData` + +On GTK and WPF it is possible to override the default `ApplicationData` folder locations using `WinRTFeatureConfiguration.ApplicationData` properties. For more information see [related docs here](/articles/features/applicationdata.md#data-location-on-gtk-and-wpf) diff --git a/doc/articles/features/applicationdata.md b/doc/articles/features/applicationdata.md index e52113b8ebc7..d7f2c222483a 100644 --- a/doc/articles/features/applicationdata.md +++ b/doc/articles/features/applicationdata.md @@ -69,9 +69,9 @@ In case of GTK and WPF targets the data are stored in application- and user-spec - `LocalFolder` - `C:\Users\UserName>\AppData\Local\\\LocalState` - `RoamingFolder` - `C:\Users\\AppData\Local\\\RoamingState` -- `LocalCahe` - `C:\Users\\AppData\Local\\\LocalCache` -- `TemporaryFolder` - `C:\Users\\AppData\Local\\\RoamingState` -- `LocalSettings` - `C:\Users\\AppData\Local\Temp\\\TempState` +- `LocalCaheFolder` - `C:\Users\\AppData\Local\\\LocalCache` +- `TemporaryFolder` - `C:\Users\\AppData\Local\Temp\\\TempState` +- `LocalSettings` - `C:\Users\\AppData\Local\\\Settings\Local.dat` - `RoamingSettings` - `C:\Users\\AppData\Local\\\Settings\Roaming.dat` **Unix-based systems** @@ -83,4 +83,15 @@ In case of GTK and WPF targets the data are stored in application- and user-spec - `LocalSettings` - `/home//.local/share///Settings/Local.dat` - `RoamingSettings` - `/home//.local/share///Settings/Roaming.dat` -Where `` is the name of the currently logged-in user and `` and `` are values coming from the `` node of the `Package.appxmanifest` (note that the publisher value is prefixed by `CN=` in the manifest, but this is excluded from the folder name). \ No newline at end of file +Where `` is the name of the currently logged-in user and `` and `` are values coming from the `` node of the `Package.appxmanifest` (note that the publisher value is prefixed by `CN=` in the manifest, but this is excluded from the folder name). + + +The default paths above can be overriden using the following feature flags: + +- `WinRTFeatureConfiguration.ApplicationData.TemporaryFolderPathOverride` - affects `TemporaryFolder` location +- `WinRTFeatureConfiguration.ApplicationData.LocalCacheFolderPathOverride` - affects `LocalCacheFolder` location +- `WinRTFeatureConfiguration.ApplicationData.ApplicationDataPathOverride` - affects `LocalFolder`, `RoamingFolder`, `LocalCaheFolder`, `LocalSettings` and `RoamingSettings` + +These properties need to be set before the application is initialized. The best place for this is `Program.cs`, before the `WpfHost` or `GtkHost` instance is created. + +If you intend to support both Windows and Unix-based systems for GTK target, make the path conditional utilizing `RuntimeInformation.IsOSPlatform(OSPlatform.Windows)`. \ No newline at end of file From 35d9770ce9434d0e6b80679c147c76033c7f5d7b Mon Sep 17 00:00:00 2001 From: Martin Zikmund Date: Thu, 27 Jul 2023 14:28:12 +0200 Subject: [PATCH 28/29] chore: Do not allow setting application data path overrides after the application is initialized --- doc/articles/features/applicationdata.md | 3 +- src/Uno.UWP/Storage/ApplicationData.cs | 3 ++ ...nRTFeatureConfiguration.ApplicationData.cs | 48 +++++++++++++++++-- 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/doc/articles/features/applicationdata.md b/doc/articles/features/applicationdata.md index d7f2c222483a..4a07f8bbd603 100644 --- a/doc/articles/features/applicationdata.md +++ b/doc/articles/features/applicationdata.md @@ -85,8 +85,7 @@ In case of GTK and WPF targets the data are stored in application- and user-spec Where `` is the name of the currently logged-in user and `` and `` are values coming from the `` node of the `Package.appxmanifest` (note that the publisher value is prefixed by `CN=` in the manifest, but this is excluded from the folder name). - -The default paths above can be overriden using the following feature flags: +The default paths above can be overridden using the following feature flags: - `WinRTFeatureConfiguration.ApplicationData.TemporaryFolderPathOverride` - affects `TemporaryFolder` location - `WinRTFeatureConfiguration.ApplicationData.LocalCacheFolderPathOverride` - affects `LocalCacheFolder` location diff --git a/src/Uno.UWP/Storage/ApplicationData.cs b/src/Uno.UWP/Storage/ApplicationData.cs index 962fa4258016..936e058bb626 100644 --- a/src/Uno.UWP/Storage/ApplicationData.cs +++ b/src/Uno.UWP/Storage/ApplicationData.cs @@ -5,6 +5,7 @@ #pragma warning disable 114 // new keyword hiding #pragma warning disable 67 // new keyword hiding using System; +using Uno; namespace Windows.Storage; @@ -38,6 +39,8 @@ private ApplicationData() _roamingSettingsLazy = new(() => new ApplicationDataContainer(this, "Roaming", ApplicationDataLocality.Roaming)); InitializePartial(); + + WinRTFeatureConfiguration.ApplicationData.IsApplicationDataInitialized = true; } partial void InitializePartial(); diff --git a/src/Uno.UWP/WinRTFeatureConfiguration.ApplicationData.cs b/src/Uno.UWP/WinRTFeatureConfiguration.ApplicationData.cs index d99c6d4007f6..4d7133924f73 100644 --- a/src/Uno.UWP/WinRTFeatureConfiguration.ApplicationData.cs +++ b/src/Uno.UWP/WinRTFeatureConfiguration.ApplicationData.cs @@ -1,5 +1,7 @@ #nullable enable +using System; + namespace Uno; partial class WinRTFeatureConfiguration @@ -7,19 +9,39 @@ partial class WinRTFeatureConfiguration public static class ApplicationData { #if __SKIA__ + private static string? _temporaryFolderPathOverride; + private static string? _localCacheFolderPathOverride; + private static string? _applicationDataPathOverride; + /// /// Allows overriding the root folder path that the application will use /// to store its TempState folder. /// /// Only applies to Skia targets. - public static string? TemporaryFolderPathOverride { get; set; } + public static string? TemporaryFolderPathOverride + { + get => _temporaryFolderPathOverride; + set + { + EnsureApplicationDataNotInitialized(); + _temporaryFolderPathOverride = value; + } + } /// /// Allows overriding the root folder path that the application will use /// to store its LocalCache folder. /// /// Only applies to Skia targets. - public static string? LocalCacheFolderPathOverride { get; set; } + public static string? LocalCacheFolderPathOverride + { + get => _localCacheFolderPathOverride; + set + { + EnsureApplicationDataNotInitialized(); + _localCacheFolderPathOverride = value; + } + } /// /// Allows overriding the root folder that the application will use @@ -27,7 +49,27 @@ public static class ApplicationData /// Only applies to Skia targets. /// /// Only applies to Skia targets. - public static string? ApplicationDataPathOverride { get; set; } + public static string? ApplicationDataPathOverride + { + get => _applicationDataPathOverride; + set + { + EnsureApplicationDataNotInitialized(); + _applicationDataPathOverride = value; + } + } + + internal static bool IsApplicationDataInitialized { get; set; } + + private static void EnsureApplicationDataNotInitialized() + { + if (IsApplicationDataInitialized) + { + throw new InvalidOperationException( + "The property was set too late in the application lifecycle." + + "Set it in Program.cs, so it is applied before ApplicationData is initialized."); + } + } #endif } } From 389c5df756f3233efb8e5568f3b103f24a6d7672 Mon Sep 17 00:00:00 2001 From: Martin Zikmund Date: Thu, 27 Jul 2023 15:01:45 +0200 Subject: [PATCH 29/29] chore: Move to skia specific --- src/Uno.UWP/Storage/ApplicationData.cs | 2 -- src/Uno.UWP/Storage/ApplicationData.skia.cs | 9 ++++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Uno.UWP/Storage/ApplicationData.cs b/src/Uno.UWP/Storage/ApplicationData.cs index 936e058bb626..2ae4bb2b7c44 100644 --- a/src/Uno.UWP/Storage/ApplicationData.cs +++ b/src/Uno.UWP/Storage/ApplicationData.cs @@ -39,8 +39,6 @@ private ApplicationData() _roamingSettingsLazy = new(() => new ApplicationDataContainer(this, "Roaming", ApplicationDataLocality.Roaming)); InitializePartial(); - - WinRTFeatureConfiguration.ApplicationData.IsApplicationDataInitialized = true; } partial void InitializePartial(); diff --git a/src/Uno.UWP/Storage/ApplicationData.skia.cs b/src/Uno.UWP/Storage/ApplicationData.skia.cs index 08f1b7ee6120..5c31757ee98c 100644 --- a/src/Uno.UWP/Storage/ApplicationData.skia.cs +++ b/src/Uno.UWP/Storage/ApplicationData.skia.cs @@ -22,12 +22,15 @@ partial class ApplicationData private static string? _appSpecificSubpath; - private string GetLocalCacheFolder() => EnsurePath(Path.Combine(GetLocalCacheFolderRootPath(), LocalCacheFolderName)); + partial void InitializePartial() => WinRTFeatureConfiguration.ApplicationData.IsApplicationDataInitialized = true; - private string GetTemporaryFolder() => EnsurePath(Path.Combine(GetTemporaryFolderRootPath(), TemporaryFolderName)); + private string GetLocalCacheFolder() => + EnsurePath(Path.Combine(GetLocalCacheFolderRootPath(), LocalCacheFolderName)); + + private string GetTemporaryFolder() => + EnsurePath(Path.Combine(GetTemporaryFolderRootPath(), TemporaryFolderName)); private string GetLocalFolder() => - // Uses XDG_DATA_HOME on Unix: https://github.com/dotnet/runtime/blob/b5705587347d29d79cec830dc22b389e1ad9a9e0/src/libraries/System.Private.CoreLib/src/System/Environment.GetFolderPathCore.Unix.cs#L105 EnsurePath(Path.Combine(GetApplicationDataFolderRootPath(), LocalFolderName)); private string GetRoamingFolder() =>