From ae981d0c12a851f778704409e1ddccd59c38056b Mon Sep 17 00:00:00 2001 From: Nick Randolph Date: Thu, 20 Jun 2024 17:03:46 +1000 Subject: [PATCH 1/3] fix: Using simple settings as alternative for key-value store when ApplicationData isn't available --- src/Uno.Extensions.Core.UI/Settings.cs | 29 +++- src/Uno.Extensions.Core/ISettings.cs | 34 ++++- .../ApplicationDataKeyValueStorage.cs | 130 ++++++++++++++---- ...EncryptedApplicationDataKeyValueStorage.cs | 5 +- .../ServiceCollectionExtensions.cs | 13 +- .../Ext/Core/Storage/StorageHostInit.cs | 18 +-- 6 files changed, 185 insertions(+), 44 deletions(-) diff --git a/src/Uno.Extensions.Core.UI/Settings.cs b/src/Uno.Extensions.Core.UI/Settings.cs index d7254e4d3e..750750f567 100644 --- a/src/Uno.Extensions.Core.UI/Settings.cs +++ b/src/Uno.Extensions.Core.UI/Settings.cs @@ -60,7 +60,7 @@ private void Initialize() return settings.TryGetValue(key, out var value) ? value : default; } } - public void Set(string key, string value) + public void Set(string key, string? value) { Initialize(); if (!_useFileSettings) @@ -82,4 +82,31 @@ public void Set(string key, string value) #endif } } + + public void Remove(string key) => Set(key, null); + + public void Clear() + { + Initialize(); + if (!_useFileSettings) + { + ApplicationData.Current.LocalSettings.Values.Clear(); + } + else + { + settings.Clear(); +#if __WINDOWS__ + File.WriteAllText(SettingsFile, JsonSerializer.Serialize(settings)); +#endif + } + } + + public IReadOnlyCollection Keys + { + get + { + Initialize(); + return _useFileSettings ? settings.Keys : ApplicationData.Current.LocalSettings.Values.Keys.Select(k => k.ToString()).ToArray(); + } + } } diff --git a/src/Uno.Extensions.Core/ISettings.cs b/src/Uno.Extensions.Core/ISettings.cs index 7bb1ab0668..64dbc4a86f 100644 --- a/src/Uno.Extensions.Core/ISettings.cs +++ b/src/Uno.Extensions.Core/ISettings.cs @@ -1,7 +1,37 @@ namespace Uno.Extensions; -internal interface ISettings +/// +/// Simple interface for storing key-value pairs. +/// +public interface ISettings { + /// + /// Gets the value associated with the specified key. + /// + /// The key of the value to get. + /// The value associated with the specified key, or null if the key does not exist. string? Get(string key); - void Set(string key, string value); + + /// + /// Sets the value associated with the specified key. + /// + /// The key of the value to set. + /// The value to set. + void Set(string key, string? value); + + /// + /// Removes the value associated with the specified key. + /// + /// The key of the value to remove. + void Remove(string key); + + /// + /// Removes all key-value pairs from the settings. + /// + void Clear(); + + /// + /// Gets a collection of all keys in the settings. + /// + IReadOnlyCollection Keys { get; } } diff --git a/src/Uno.Extensions.Storage.UI/KeyValueStorage/ApplicationDataKeyValueStorage.cs b/src/Uno.Extensions.Storage.UI/KeyValueStorage/ApplicationDataKeyValueStorage.cs index ad37dced48..503c5b9689 100644 --- a/src/Uno.Extensions.Storage.UI/KeyValueStorage/ApplicationDataKeyValueStorage.cs +++ b/src/Uno.Extensions.Storage.UI/KeyValueStorage/ApplicationDataKeyValueStorage.cs @@ -4,18 +4,75 @@ internal record ApplicationDataKeyValueStorage (ILogger Logger, InMemoryKeyValueStorage InMemoryStorage, KeyValueStorageSettings Settings, - ISerializer Serializer) : BaseKeyValueStorageWithCaching(InMemoryStorage, Settings) + ISerializer Serializer, + ISettings UnpackagedSettings + ) : BaseKeyValueStorageWithCaching(InMemoryStorage, Settings) { public const string Name = "ApplicationData"; // Do not change this value. private const string KeyNameSuffix = "_ADCSSS"; - private readonly ApplicationDataContainer _dataContainer = ApplicationData.Current.LocalSettings; + private ApplicationDataContainer DataContainer => ApplicationData.Current.LocalSettings; /// public override bool IsEncrypted => false; + private bool UseApplicationData => +#if !WINDOWS + true; +#else + PlatformHelper.IsAppPackaged; +#endif + private bool TryGetSetting(string name, out object? value) + { + if (UseApplicationData) + { + return DataContainer.Values.TryGetValue(GetKey(name), out value); + } + else + { + return UnpackagedSettings.Get(name) is string t + ? (value = t) is not null + : (value = null) is not null; + } + } + private void RemoveSetting(string name) + { + if (UseApplicationData) + { + _ = DataContainer.Values.Remove(GetKey(name)); + } + else + { + UnpackagedSettings.Remove(name); + } + } + + private void SetSetting(string name, object? value) + { + if (UseApplicationData) + { + DataContainer.Values[GetKey(name)] = value; + } + else + { + UnpackagedSettings.Set(name, value?.ToString()); + } + } + + private void ClearSettings() + { + if (UseApplicationData) + { + DataContainer.Values.Clear(); + } + else + { + UnpackagedSettings.Clear(); + } + } + /// protected override async ValueTask InternalClearAsync(string? name, CancellationToken ct) { @@ -27,13 +84,11 @@ protected override async ValueTask InternalClearAsync(string? name, Cancellation if (name is not null && !string.IsNullOrEmpty(name)) { - _ = _dataContainer.Values.Remove(GetKey(name)); - + RemoveSetting(name); } else { - _dataContainer.Values.Clear(); - + ClearSettings(); } if (Logger.IsEnabled(LogLevel.Information)) @@ -46,46 +101,71 @@ protected override async ValueTask InternalClearAsync(string? name, Cancellation /// protected override async ValueTask InternalGetKeysAsync(CancellationToken ct) { - return _dataContainer + if (UseApplicationData) + { + return DataContainer .Values .Keys - .Where(key=>key.EndsWith(KeyNameSuffix)) - .Select(key => GetName(key)) // filter-out non-encrypted storage + .Where(key => key.EndsWith(KeyNameSuffix)) + .Select(GetName) // filter-out non-encrypted storage + .Trim() + .ToArray(); + } + else + { + return UnpackagedSettings + .Keys + .Where(key => key.EndsWith(KeyNameSuffix)) + .Select(GetName) // filter-out non-encrypted storage .Trim() .ToArray(); + + } } /// #nullable disable protected override async ValueTask InternalGetAsync(string name, CancellationToken ct) { - if (Logger.IsEnabled(LogLevel.Debug)) + try { - Logger.LogDebugMessage($"Getting value for key '{name}'."); - } + if (Logger.IsEnabled(LogLevel.Debug)) + { + Logger.LogDebugMessage($"Getting value for key '{name}'."); + } - if (!_dataContainer.Values.TryGetValue(GetKey(name), out var data)) - { - throw new KeyNotFoundException(name); - } + if (!TryGetSetting(GetKey(name), out var data)) + { + throw new KeyNotFoundException(name); + } - var value = await GetTypedValue(data,ct); + var value = await GetTypedValue(data, ct); - if (Logger.IsEnabled(LogLevel.Information)) - { - Logger.LogInformationMessage($"Retrieved value for key '{name}'."); + if (Logger.IsEnabled(LogLevel.Information)) + { + Logger.LogInformationMessage($"Retrieved value for key '{name}'."); + } + + return value; } + catch (Exception ex) + { + if (Logger.IsEnabled(LogLevel.Warning)) + { + Logger.LogWarningMessage($"Error getting value for key '{name}'.", ex.Message); + } - return value; + return default; + } } #nullable restore - protected virtual async Task GetTypedValue(object? data, CancellationToken ct) + protected virtual async Task GetTypedValue(object? data, CancellationToken ct) { return this.Deserialize(data as string); } - protected virtual async Task GetObjectValue(T data, CancellationToken ct) where T :notnull + protected virtual async Task GetObjectValue(T data, CancellationToken ct) where T : notnull { return this.Serialize(data); } @@ -98,8 +178,8 @@ protected override async ValueTask InternalSetAsync(string name, T value, Can Logger.LogDebugMessage($"Setting value for key '{name}'."); } - var data= await GetObjectValue(value, ct); - _dataContainer.Values[GetKey(name)] = data; + var data = await GetObjectValue(value, ct); + SetSetting(GetKey(name), data); if (Logger.IsEnabled(LogLevel.Information)) { diff --git a/src/Uno.Extensions.Storage.UI/KeyValueStorage/EncryptedApplicationDataKeyValueStorage.cs b/src/Uno.Extensions.Storage.UI/KeyValueStorage/EncryptedApplicationDataKeyValueStorage.cs index 8b1cee0cfd..c1ecd500f1 100644 --- a/src/Uno.Extensions.Storage.UI/KeyValueStorage/EncryptedApplicationDataKeyValueStorage.cs +++ b/src/Uno.Extensions.Storage.UI/KeyValueStorage/EncryptedApplicationDataKeyValueStorage.cs @@ -10,8 +10,9 @@ internal record EncryptedApplicationDataKeyValueStorage( ILogger EncryptedLogger, InMemoryKeyValueStorage InMemoryStorage, KeyValueStorageSettings Settings, - ISerializer Serializer) - : ApplicationDataKeyValueStorage(EncryptedLogger, InMemoryStorage, Settings, Serializer) + ISerializer Serializer, + ISettings UnpackagedSettings) + : ApplicationDataKeyValueStorage(EncryptedLogger, InMemoryStorage, Settings, Serializer, UnpackagedSettings) { public new const string Name = "EncryptedApplicationData"; diff --git a/src/Uno.Extensions.Storage.UI/ServiceCollectionExtensions.cs b/src/Uno.Extensions.Storage.UI/ServiceCollectionExtensions.cs index ea7557e91e..46487244de 100644 --- a/src/Uno.Extensions.Storage.UI/ServiceCollectionExtensions.cs +++ b/src/Uno.Extensions.Storage.UI/ServiceCollectionExtensions.cs @@ -9,14 +9,15 @@ public static IServiceCollection AddFileStorage(this IServiceCollection services private static TKeyValueStorage CreateKeyValueStorage( this IServiceProvider sp, string name, - Func, InMemoryKeyValueStorage, KeyValueStorageSettings, ISerializer, TKeyValueStorage> creator) + Func, InMemoryKeyValueStorage, KeyValueStorageSettings, ISerializer, ISettings, TKeyValueStorage> creator) { var l = sp.GetRequiredService>(); var s = sp.GetRequiredService(); var inmem = sp.GetRequiredService(); var config = sp.GetRequiredService>(); var settings = config.Value.GetSettingsOrDefault(name); - return creator(l, inmem, settings, s); + var unpackaged = sp.GetRequiredService(); + return creator(l, inmem, settings, s, unpackaged); } public static IServiceCollection AddKeyedStorage(this IServiceCollection services) @@ -27,7 +28,7 @@ public static IServiceCollection AddKeyedStorage(this IServiceCollection service ApplicationDataKeyValueStorage.Name, sp => sp.CreateKeyValueStorage( ApplicationDataKeyValueStorage.Name, - (l,inmem, settings,s)=>new ApplicationDataKeyValueStorage(l, inmem, settings, s) + (l, inmem, settings, s, unpackaged) => new ApplicationDataKeyValueStorage(l, inmem, settings, s, unpackaged) ) ) #if __ANDROID__ @@ -35,7 +36,7 @@ public static IServiceCollection AddKeyedStorage(this IServiceCollection service KeyStoreKeyValueStorage.Name, sp => sp.CreateKeyValueStorage( KeyStoreKeyValueStorage.Name, - (l, inmem, settings, s) => new KeyStoreKeyValueStorage(l, inmem, settings, s) + (l, inmem, settings, s, unpackaged) => new KeyStoreKeyValueStorage(l, inmem, settings, s) ) ) #endif @@ -44,7 +45,7 @@ public static IServiceCollection AddKeyedStorage(this IServiceCollection service KeyChainKeyValueStorage.Name, sp => sp.CreateKeyValueStorage( KeyChainKeyValueStorage.Name, - (l, inmem, settings, s) => new KeyChainKeyValueStorage(l, inmem, settings, s) + (l, inmem, settings, s, unpackaged) => new KeyChainKeyValueStorage(l, inmem, settings, s) ) ) #endif @@ -57,7 +58,7 @@ public static IServiceCollection AddKeyedStorage(this IServiceCollection service EncryptedApplicationDataKeyValueStorage.Name, sp => sp.CreateKeyValueStorage( EncryptedApplicationDataKeyValueStorage.Name, - (l, inmem, settings, s) => new EncryptedApplicationDataKeyValueStorage(l, inmem, settings, s) + (l, inmem, settings, s, unpackaged) => new EncryptedApplicationDataKeyValueStorage(l, inmem, settings, s, unpackaged) ) ) #endif diff --git a/testing/TestHarness/TestHarness.Shared/Ext/Core/Storage/StorageHostInit.cs b/testing/TestHarness/TestHarness.Shared/Ext/Core/Storage/StorageHostInit.cs index c431ebb967..fcd2fb72a9 100644 --- a/testing/TestHarness/TestHarness.Shared/Ext/Core/Storage/StorageHostInit.cs +++ b/testing/TestHarness/TestHarness.Shared/Ext/Core/Storage/StorageHostInit.cs @@ -1,6 +1,4 @@ -using System.Collections.Immutable; -using Microsoft.Extensions.Options; -using Uno.Extensions.Storage.KeyValueStorage; +using Uno.Extensions.Storage.KeyValueStorage; namespace TestHarness; @@ -22,7 +20,8 @@ protected override IHostBuilder Custom(IHostBuilder builder) var s = sp.GetRequiredService(); var config = sp.GetRequiredService>(); var settings = config.Value.GetSettingsOrDefault(NoCacheStorage); - return new TestingKeyValueStorage(l,inmem, settings, s); + var unpackaged = sp.GetRequiredService(); + return new TestingKeyValueStorage(l, inmem, settings, s, unpackaged); })); } @@ -39,7 +38,7 @@ protected override void RegisterRoutes(IViewRegistry views, IRouteRegistry route } } -internal record TestingKeyValueStorage +internal record TestingKeyValueStorage #if __ANDROID__ : KeyStoreKeyValueStorage { @@ -47,7 +46,8 @@ public TestingKeyValueStorage( ILogger logger, InMemoryKeyValueStorage inmem, KeyValueStorageSettings settings, - ISerializer serializer) : base(logger, inmem, settings, serializer) + ISerializer serializer, + ISettings UnpackagedSettings) : base(logger, inmem, settings, serializer) { } @@ -55,14 +55,16 @@ public TestingKeyValueStorage( (ILogger TestingLogger, InMemoryKeyValueStorage InMemoryStorage, KeyValueStorageSettings Settings, - ISerializer Serializer) : KeyChainKeyValueStorage(TestingLogger, InMemoryStorage, Settings, Serializer) + ISerializer Serializer, + ISettings UnpackagedSettings) : KeyChainKeyValueStorage(TestingLogger, InMemoryStorage, Settings, Serializer) { #else (ILogger TestingLogger, InMemoryKeyValueStorage InMemoryStorage, KeyValueStorageSettings Settings, - ISerializer Serializer) : ApplicationDataKeyValueStorage(TestingLogger, InMemoryStorage, Settings, Serializer) + ISerializer Serializer, + ISettings UnpackagedSettings) : ApplicationDataKeyValueStorage(TestingLogger, InMemoryStorage, Settings, Serializer, UnpackagedSettings) { #endif From 58e43388a905d6100d1157c222336919f5b6efc9 Mon Sep 17 00:00:00 2001 From: Nick Randolph Date: Fri, 21 Jun 2024 12:41:32 +1000 Subject: [PATCH 2/3] chore: setting cultureinfo on tostring Co-authored-by: Carl de Billy --- src/Uno.Extensions.Core.UI/Settings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Uno.Extensions.Core.UI/Settings.cs b/src/Uno.Extensions.Core.UI/Settings.cs index 750750f567..904c585220 100644 --- a/src/Uno.Extensions.Core.UI/Settings.cs +++ b/src/Uno.Extensions.Core.UI/Settings.cs @@ -106,7 +106,7 @@ public IReadOnlyCollection Keys get { Initialize(); - return _useFileSettings ? settings.Keys : ApplicationData.Current.LocalSettings.Values.Keys.Select(k => k.ToString()).ToArray(); + return _useFileSettings ? settings.Keys : ApplicationData.Current.LocalSettings.Values.Keys.Select(k => k.ToString(CultureInfo.InvariableCulture)).ToArray(); } } } From a1fbcd2925f4f43527815c5bccc81867cd341741 Mon Sep 17 00:00:00 2001 From: Nick Randolph Date: Mon, 24 Jun 2024 14:57:23 +1000 Subject: [PATCH 3/3] chore: Fix build --- src/Uno.Extensions.Core.UI/Settings.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Uno.Extensions.Core.UI/Settings.cs b/src/Uno.Extensions.Core.UI/Settings.cs index 904c585220..cec3fe9882 100644 --- a/src/Uno.Extensions.Core.UI/Settings.cs +++ b/src/Uno.Extensions.Core.UI/Settings.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System.Globalization; +using System.Text.Json; namespace Uno.Extensions; @@ -106,7 +107,7 @@ public IReadOnlyCollection Keys get { Initialize(); - return _useFileSettings ? settings.Keys : ApplicationData.Current.LocalSettings.Values.Keys.Select(k => k.ToString(CultureInfo.InvariableCulture)).ToArray(); + return _useFileSettings ? settings.Keys : ApplicationData.Current.LocalSettings.Values.Keys.Select(k => k.ToString(CultureInfo.InvariantCulture)).ToArray(); } } }