From 3fedf5c94d86524c0267788923e121b75f988ccf Mon Sep 17 00:00:00 2001 From: Lamparter <71598437+Lamparter@users.noreply.github.com> Date: Tue, 7 Jan 2025 18:40:53 +0000 Subject: [PATCH 1/4] Port `StorageFileHelper` --- components/Helpers/src/StorageFileHelper.cs | 747 ++++++++++++++++++++ 1 file changed, 747 insertions(+) create mode 100644 components/Helpers/src/StorageFileHelper.cs diff --git a/components/Helpers/src/StorageFileHelper.cs b/components/Helpers/src/StorageFileHelper.cs new file mode 100644 index 00000000..d40b9e41 --- /dev/null +++ b/components/Helpers/src/StorageFileHelper.cs @@ -0,0 +1,747 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Windows.ApplicationModel; +using Windows.Storage; +using Windows.Storage.Search; +using Windows.Storage.Streams; + +namespace CommunityToolkit.WinUI.Helpers; + +/// +/// This class provides static helper methods for . +/// +public static class StorageFileHelper +{ + /// + /// Saves a string value to a in application local folder/>. + /// + /// + /// The value to save to the file. + /// + /// + /// The name for the file. + /// + /// + /// The creation collision options. Default is ReplaceExisting. + /// + /// + /// The saved containing the text. + /// + /// + /// Exception thrown if the file location or file name are null or empty. + /// + public static Task WriteTextToLocalFileAsync( + string text, + string fileName, + CreationCollisionOption options = CreationCollisionOption.ReplaceExisting) + { + if (string.IsNullOrWhiteSpace(fileName)) + { + throw new ArgumentNullException(nameof(fileName)); + } + + var folder = ApplicationData.Current.LocalFolder; + return folder.WriteTextToFileAsync(text, fileName, options); + } + + /// + /// Saves a string value to a in application local cache folder/>. + /// + /// + /// The value to save to the file. + /// + /// + /// The name for the file. + /// + /// + /// The creation collision options. Default is ReplaceExisting. + /// + /// + /// The saved containing the text. + /// + /// + /// Exception thrown if the file location or file name are null or empty. + /// + public static Task WriteTextToLocalCacheFileAsync( + string text, + string fileName, + CreationCollisionOption options = CreationCollisionOption.ReplaceExisting) + { + if (string.IsNullOrWhiteSpace(fileName)) + { + throw new ArgumentNullException(nameof(fileName)); + } + + var folder = ApplicationData.Current.LocalCacheFolder; + return folder.WriteTextToFileAsync(text, fileName, options); + } + +#if !HAS_UNO + /// + /// Saves a string value to a in well known folder/>. + /// + /// + /// The well known folder ID to use. + /// + /// + /// The value to save to the file. + /// + /// + /// The name for the file. + /// + /// + /// The creation collision options. Default is ReplaceExisting. + /// + /// + /// The saved containing the text. + /// + /// + /// Exception thrown if the file location or file name are null or empty. + /// + public static Task WriteTextToKnownFolderFileAsync( + KnownFolderId knownFolderId, + string text, + string fileName, + CreationCollisionOption options = CreationCollisionOption.ReplaceExisting) + { + if (string.IsNullOrWhiteSpace(fileName)) + { + throw new ArgumentNullException(nameof(fileName)); + } + + var folder = GetFolderFromKnownFolderId(knownFolderId); + return folder.WriteTextToFileAsync(text, fileName, options); + } +#endif + + /// + /// Saves a string value to a in the given . + /// + /// + /// The to save the file in. + /// + /// + /// The value to save to the file. + /// + /// + /// The name for the file. + /// + /// + /// The creation collision options. Default is ReplaceExisting. + /// + /// + /// The saved containing the text. + /// + /// + /// Exception thrown if the file location or file name are null or empty. + /// + public static async Task WriteTextToFileAsync( + this StorageFolder fileLocation, + string text, + string fileName, + CreationCollisionOption options = CreationCollisionOption.ReplaceExisting) + { + if (fileLocation == null) + { + throw new ArgumentNullException(nameof(fileLocation)); + } + + if (string.IsNullOrWhiteSpace(fileName)) + { + throw new ArgumentNullException(nameof(fileName)); + } + + var storageFile = await fileLocation.CreateFileAsync(fileName, options); + await FileIO.WriteTextAsync(storageFile, text); + + return storageFile; + } + + /// + /// Saves an array of bytes to a to application local folder/>. + /// + /// + /// The array to save to the file. + /// + /// + /// The name for the file. + /// + /// + /// The creation collision options. Default is ReplaceExisting. + /// + /// + /// The saved containing the bytes. + /// + /// + /// Exception thrown if the file location or file name are null or empty. + /// + public static Task WriteBytesToLocalFileAsync( + byte[] bytes, + string fileName, + CreationCollisionOption options = CreationCollisionOption.ReplaceExisting) + { + if (string.IsNullOrWhiteSpace(fileName)) + { + throw new ArgumentNullException(nameof(fileName)); + } + + var folder = ApplicationData.Current.LocalFolder; + return folder.WriteBytesToFileAsync(bytes, fileName, options); + } + + /// + /// Saves an array of bytes to a to application local cache folder/>. + /// + /// + /// The array to save to the file. + /// + /// + /// The name for the file. + /// + /// + /// The creation collision options. Default is ReplaceExisting. + /// + /// + /// The saved containing the bytes. + /// + /// + /// Exception thrown if the file location or file name are null or empty. + /// + public static Task WriteBytesToLocalCacheFileAsync( + byte[] bytes, + string fileName, + CreationCollisionOption options = CreationCollisionOption.ReplaceExisting) + { + if (string.IsNullOrWhiteSpace(fileName)) + { + throw new ArgumentNullException(nameof(fileName)); + } + + var folder = ApplicationData.Current.LocalCacheFolder; + return folder.WriteBytesToFileAsync(bytes, fileName, options); + } + +#if !HAS_UNO + /// + /// Saves an array of bytes to a to well known folder/>. + /// + /// + /// The well known folder ID to use. + /// + /// + /// The array to save to the file. + /// + /// + /// The name for the file. + /// + /// + /// The creation collision options. Default is ReplaceExisting. + /// + /// + /// The saved containing the bytes. + /// + /// + /// Exception thrown if the file location or file name are null or empty. + /// + public static Task WriteBytesToKnownFolderFileAsync( + KnownFolderId knownFolderId, + byte[] bytes, + string fileName, + CreationCollisionOption options = CreationCollisionOption.ReplaceExisting) + { + if (string.IsNullOrWhiteSpace(fileName)) + { + throw new ArgumentNullException(nameof(fileName)); + } + + var folder = GetFolderFromKnownFolderId(knownFolderId); + return folder.WriteBytesToFileAsync(bytes, fileName, options); + } +#endif + + /// + /// Saves an array of bytes to a in the given . + /// + /// + /// The to save the file in. + /// + /// + /// The array to save to the file. + /// + /// + /// The name for the file. + /// + /// + /// The creation collision options. Default is ReplaceExisting. + /// + /// + /// The saved containing the bytes. + /// + /// + /// Exception thrown if the file location or file name are null or empty. + /// + public static async Task WriteBytesToFileAsync( + this StorageFolder fileLocation, + byte[] bytes, + string fileName, + CreationCollisionOption options = CreationCollisionOption.ReplaceExisting) + { + if (fileLocation == null) + { + throw new ArgumentNullException(nameof(fileLocation)); + } + + if (string.IsNullOrWhiteSpace(fileName)) + { + throw new ArgumentNullException(nameof(fileName)); + } + + var storageFile = await fileLocation.CreateFileAsync(fileName, options); + await FileIO.WriteBytesAsync(storageFile, bytes); + + return storageFile; + } + + /// + /// Gets a string value from a located in the application installation folder. + /// + /// + /// The relative file path. + /// + /// + /// The stored value. + /// + /// + /// Exception thrown if the is null or empty. + /// + public static Task ReadTextFromPackagedFileAsync(string fileName) + { + if (string.IsNullOrWhiteSpace(fileName)) + { + throw new ArgumentNullException(nameof(fileName)); + } + + var folder = Package.Current.InstalledLocation; + return folder.ReadTextFromFileAsync(fileName); + } + + /// + /// Gets a string value from a located in the application local cache folder. + /// + /// + /// The relative file path. + /// + /// + /// The stored value. + /// + /// + /// Exception thrown if the is null or empty. + /// + public static Task ReadTextFromLocalCacheFileAsync(string fileName) + { + if (string.IsNullOrWhiteSpace(fileName)) + { + throw new ArgumentNullException(nameof(fileName)); + } + + var folder = ApplicationData.Current.LocalCacheFolder; + return folder.ReadTextFromFileAsync(fileName); + } + + /// + /// Gets a string value from a located in the application local folder. + /// + /// + /// The relative file path. + /// + /// + /// The stored value. + /// + /// + /// Exception thrown if the is null or empty. + /// + public static Task ReadTextFromLocalFileAsync(string fileName) + { + if (string.IsNullOrWhiteSpace(fileName)) + { + throw new ArgumentNullException(nameof(fileName)); + } + + var folder = ApplicationData.Current.LocalFolder; + return folder.ReadTextFromFileAsync(fileName); + } + +#if !HAS_UNO + /// + /// Gets a string value from a located in a well known folder. + /// + /// + /// The well known folder ID to use. + /// + /// + /// The relative file path. + /// + /// + /// The stored value. + /// + /// + /// Exception thrown if the is null or empty. + /// + public static Task ReadTextFromKnownFoldersFileAsync( + KnownFolderId knownFolderId, + string fileName) + { + if (string.IsNullOrWhiteSpace(fileName)) + { + throw new ArgumentNullException(nameof(fileName)); + } + + var folder = GetFolderFromKnownFolderId(knownFolderId); + return folder.ReadTextFromFileAsync(fileName); + } +#endif + + /// + /// Gets a string value from a located in the given . + /// + /// + /// The to save the file in. + /// + /// + /// The relative file path. + /// + /// + /// The stored value. + /// + /// + /// Exception thrown if the is null or empty. + /// + public static async Task ReadTextFromFileAsync( + this StorageFolder fileLocation, + string fileName) + { + if (string.IsNullOrWhiteSpace(fileName)) + { + throw new ArgumentNullException(nameof(fileName)); + } + + var file = await fileLocation.GetFileAsync(fileName); + return await FileIO.ReadTextAsync(file); + } + +#if !HAS_UNO + /// + /// Gets an array of bytes from a located in the application installation folder. + /// + /// + /// The relative file path. + /// + /// + /// The stored array. + /// + /// + /// Exception thrown if the is null or empty. + /// + public static Task ReadBytesFromPackagedFileAsync(string fileName) + { + if (string.IsNullOrWhiteSpace(fileName)) + { + throw new ArgumentNullException(nameof(fileName)); + } + + var folder = Package.Current.InstalledLocation; + return folder.ReadBytesFromFileAsync(fileName); + } + + /// + /// Gets an array of bytes from a located in the application local cache folder. + /// + /// + /// The relative file path. + /// + /// + /// The stored array. + /// + /// + /// Exception thrown if the is null or empty. + /// + public static Task ReadBytesFromLocalCacheFileAsync(string fileName) + { + if (string.IsNullOrWhiteSpace(fileName)) + { + throw new ArgumentNullException(nameof(fileName)); + } + + var folder = ApplicationData.Current.LocalCacheFolder; + return folder.ReadBytesFromFileAsync(fileName); + } + + /// + /// Gets an array of bytes from a located in the application local folder. + /// + /// + /// The relative file path. + /// + /// + /// The stored array. + /// + /// + /// Exception thrown if the is null or empty. + /// + public static Task ReadBytesFromLocalFileAsync(string fileName) + { + if (string.IsNullOrWhiteSpace(fileName)) + { + throw new ArgumentNullException(nameof(fileName)); + } + + var folder = ApplicationData.Current.LocalFolder; + return folder.ReadBytesFromFileAsync(fileName); + } + + /// + /// Gets an array of bytes from a located in a well known folder. + /// + /// + /// The well known folder ID to use. + /// + /// + /// The relative file path. + /// + /// + /// The stored array. + /// + /// + /// Exception thrown if the is null or empty. + /// + public static Task ReadBytesFromKnownFoldersFileAsync( + KnownFolderId knownFolderId, + string fileName) + { + if (string.IsNullOrWhiteSpace(fileName)) + { + throw new ArgumentNullException(nameof(fileName)); + } + + var folder = GetFolderFromKnownFolderId(knownFolderId); + return folder.ReadBytesFromFileAsync(fileName); + } + + /// + /// Gets an array of bytes from a located in the given . + /// + /// + /// The to save the file in. + /// + /// + /// The relative file path. + /// + /// + /// The stored array. + /// + /// + /// Exception thrown if the is null or empty. + /// + public static async Task ReadBytesFromFileAsync( + this StorageFolder fileLocation, + string fileName) + { + if (string.IsNullOrWhiteSpace(fileName)) + { + throw new ArgumentNullException(nameof(fileName)); + } + + var file = await fileLocation.GetFileAsync(fileName).AsTask().ConfigureAwait(false); + return await file.ReadBytesAsync(); + } + + /// + /// Gets an array of bytes from a . + /// + /// + /// The . + /// + /// + /// The stored array. + /// + public static async Task ReadBytesAsync(this StorageFile file) + { + if (file == null) + { + throw new ArgumentNullException(nameof(file)); + } + + using (IRandomAccessStream stream = await file.OpenReadAsync()) + { + using (var reader = new DataReader(stream.GetInputStreamAt(0))) + { + await reader.LoadAsync((uint)stream.Size); + var bytes = new byte[stream.Size]; + reader.ReadBytes(bytes); + return bytes; + } + } + } +#endif + + /// + /// Gets a value indicating whether a file exists in the current folder. + /// + /// + /// The to look for the file in. + /// + /// + /// The filename of the file to search for. Must include the file extension and is not case-sensitive. + /// + /// + /// The , indicating if the subfolders should also be searched through. + /// + /// + /// true if the file exists; otherwise, false. + /// + public static Task FileExistsAsync(this StorageFolder folder, string fileName, bool isRecursive = false) +#if HAS_UNO + => FileExistsInFolderAsync(folder, fileName); +#else + => isRecursive + ? FileExistsInSubtreeAsync(folder, fileName) + : FileExistsInFolderAsync(folder, fileName); +#endif + + /// + /// Gets a value indicating whether a filename is correct or not using the Storage feature. + /// + /// The filename to test. Must include the file extension and is not case-sensitive. + /// true if the filename is valid; otherwise, false. + public static bool IsFileNameValid(string fileName) + { + var illegalChars = Path.GetInvalidFileNameChars(); + return fileName.All(c => !illegalChars.Contains(c)); + } + + /// + /// Gets a value indicating whether a file path is correct or not using the Storage feature. + /// + /// The file path to test. Must include the file extension and is not case-sensitive. + /// true if the file path is valid; otherwise, false. + public static bool IsFilePathValid(string filePath) + { + var illegalChars = Path.GetInvalidPathChars(); + return filePath.All(c => !illegalChars.Contains(c)); + } + + /// + /// Gets a value indicating whether a file exists in the current folder. + /// + /// + /// The to look for the file in. + /// + /// + /// The filename of the file to search for. Must include the file extension and is not case-sensitive. + /// + /// + /// true if the file exists; otherwise, false. + /// + internal static async Task FileExistsInFolderAsync(StorageFolder folder, string fileName) + { + var item = await folder.TryGetItemAsync(fileName).AsTask().ConfigureAwait(false); + return item != null && item.IsOfType(StorageItemTypes.File); + } + +#if !HAS_UNO + /// + /// Gets a value indicating whether a file exists in the current folder or in one of its subfolders. + /// + /// + /// The to look for the file in. + /// + /// + /// The filename of the file to search for. Must include the file extension and is not case-sensitive. + /// + /// + /// true if the file exists; otherwise, false. + /// + /// + /// Exception thrown if the contains a quotation mark. + /// + internal static async Task FileExistsInSubtreeAsync(StorageFolder rootFolder, string fileName) + { + if (fileName.IndexOf('"') >= 0) + { + throw new ArgumentException(nameof(fileName)); + } + + var options = new QueryOptions + { + FolderDepth = FolderDepth.Deep, + UserSearchFilter = $"filename:=\"{fileName}\"" // “:=” is the exact-match operator + }; + + var files = await rootFolder.CreateFileQueryWithOptions(options).GetFilesAsync().AsTask().ConfigureAwait(false); + return files.Count > 0; + } + + /// + /// Returns a from a + /// + /// Folder Id + /// The + internal static StorageFolder GetFolderFromKnownFolderId(KnownFolderId knownFolderId) + { + StorageFolder workingFolder; + + switch (knownFolderId) + { + case KnownFolderId.AppCaptures: + workingFolder = KnownFolders.AppCaptures; + break; + case KnownFolderId.CameraRoll: + workingFolder = KnownFolders.CameraRoll; + break; + case KnownFolderId.DocumentsLibrary: + workingFolder = KnownFolders.DocumentsLibrary; + break; + case KnownFolderId.HomeGroup: + workingFolder = KnownFolders.HomeGroup; + break; + case KnownFolderId.MediaServerDevices: + workingFolder = KnownFolders.MediaServerDevices; + break; + case KnownFolderId.MusicLibrary: + workingFolder = KnownFolders.MusicLibrary; + break; + case KnownFolderId.Objects3D: + workingFolder = KnownFolders.Objects3D; + break; + case KnownFolderId.PicturesLibrary: + workingFolder = KnownFolders.PicturesLibrary; + break; + case KnownFolderId.Playlists: + workingFolder = KnownFolders.Playlists; + break; + case KnownFolderId.RecordedCalls: + workingFolder = KnownFolders.RecordedCalls; + break; + case KnownFolderId.RemovableDevices: + workingFolder = KnownFolders.RemovableDevices; + break; + case KnownFolderId.SavedPictures: + workingFolder = KnownFolders.SavedPictures; + break; + case KnownFolderId.VideosLibrary: + workingFolder = KnownFolders.VideosLibrary; + break; + default: + throw new ArgumentOutOfRangeException(nameof(knownFolderId), knownFolderId, null); + } + + return workingFolder; + } +#endif +} From 546c6ed6934e0d0d7e30d2080d8a7d8173dda92e Mon Sep 17 00:00:00 2001 From: Lamparter <71598437+Lamparter@users.noreply.github.com> Date: Tue, 7 Jan 2025 18:45:36 +0000 Subject: [PATCH 2/4] Port `ApplicationDataStorageHelper` very difficult --- .../ApplicationDataStorageHelper.cs | 344 ++++++++++++++++++ 1 file changed, 344 insertions(+) create mode 100644 components/Helpers/src/ObjectStorage/ApplicationDataStorageHelper.cs diff --git a/components/Helpers/src/ObjectStorage/ApplicationDataStorageHelper.cs b/components/Helpers/src/ObjectStorage/ApplicationDataStorageHelper.cs new file mode 100644 index 00000000..2912716d --- /dev/null +++ b/components/Helpers/src/ObjectStorage/ApplicationDataStorageHelper.cs @@ -0,0 +1,344 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using CommunityToolkit.Common.Helpers; +using Windows.Storage; +using Windows.System; +using CommunityToolkit.Helpers; + +namespace CommunityToolkit.WinUI.Helpers.ObjectStorage; + +/// +/// Storage helper for files and folders living in Windows.Storage.ApplicationData storage endpoints. +/// +/// The data store to interact with. +/// Serializer for converting stored values. Defaults to . +public partial class ApplicationDataStorageHelper(ApplicationData appData, IObjectSerializer? objectSerializer = null) : IFileStorageHelper, ISettingsStorageHelper +{ + /// + /// Gets the settings container. + /// + public ApplicationDataContainer Settings => AppData.LocalSettings; + + /// + /// Gets the storage folder. + /// + public StorageFolder Folder => AppData.LocalFolder; + + /// + /// Gets the storage host. + /// + protected ApplicationData AppData { get; } = appData ?? throw new ArgumentNullException(nameof(appData)); + + /// + /// Gets the serializer for converting stored values. + /// + protected IObjectSerializer Serializer { get; } = objectSerializer ?? new SystemSerializer(); + + /// + /// Get a new instance using ApplicationData.Current and the provided serializer. + /// + /// Serializer for converting stored values. Defaults to . + /// A new instance of ApplicationDataStorageHelper. + public static ApplicationDataStorageHelper GetCurrent(IObjectSerializer? objectSerializer = null) + { + var appData = ApplicationData.Current; + return new ApplicationDataStorageHelper(appData, objectSerializer); + } + +#if !HAS_UNO + /// + /// Get a new instance using the ApplicationData for the provided user and serializer. + /// + /// App data user owner. + /// Serializer for converting stored values. Defaults to . + /// A new instance of ApplicationDataStorageHelper. + public static async Task GetForUserAsync(User user, IObjectSerializer? objectSerializer = null) + { + var appData = await ApplicationData.GetForUserAsync(user); + return new ApplicationDataStorageHelper(appData, objectSerializer); + } +#endif + + /// + /// Determines whether a setting already exists. + /// + /// Key of the setting (that contains object). + /// True if a value exists. + public bool KeyExists(string key) + { + return Settings.Values.ContainsKey(key); + } + + /// + /// Retrieves a single item by its key. + /// + /// Type of object retrieved. + /// Key of the object. + /// Default value of the object. + /// The TValue object. + public T? Read(string key, T? @default = default) + { + if (Settings.Values.TryGetValue(key, out var valueObj) && valueObj is string valueString) + { + return Serializer.Deserialize(valueString); + } + + return @default; + } + + /// + public bool TryRead(string key, out T? value) + { + if (Settings.Values.TryGetValue(key, out var valueObj) && valueObj is string valueString) + { + value = Serializer.Deserialize(valueString); + return true; + } + + value = default; + return false; + } + + /// + public void Save(string key, T value) + { + Settings.Values[key] = Serializer.Serialize(value); + } + + /// + public bool TryDelete(string key) + { + return Settings.Values.Remove(key); + } + + /// + public void Clear() + { + Settings.Values.Clear(); + } + + /// + /// Determines whether a setting already exists in composite. + /// + /// Key of the composite (that contains settings). + /// Key of the setting (that contains object). + /// True if a value exists. + public bool KeyExists(string compositeKey, string key) + { + if (TryRead(compositeKey, out ApplicationDataCompositeValue? composite) && composite != null) + { + return composite.ContainsKey(key); + } + + return false; + } + + /// + /// Attempts to retrieve a single item by its key in composite. + /// + /// Type of object retrieved. + /// Key of the composite (that contains settings). + /// Key of the object. + /// The value of the object retrieved. + /// The T object. + public bool TryRead(string compositeKey, string key, out T? value) + { + if (TryRead(compositeKey, out ApplicationDataCompositeValue? composite) && composite != null) + { + string compositeValue = (string)composite[key]; + if (compositeValue != null) + { + value = Serializer.Deserialize(compositeValue); + return true; + } + } + + value = default; + return false; + } + + /// + /// Retrieves a single item by its key in composite. + /// + /// Type of object retrieved. + /// Key of the composite (that contains settings). + /// Key of the object. + /// Default value of the object. + /// The T object. + public T? Read(string compositeKey, string key, T? @default = default) + { + if (TryRead(compositeKey, out ApplicationDataCompositeValue? composite) && composite != null) + { + if (composite.TryGetValue(key, out var valueObj) && valueObj is string value) + { + return Serializer.Deserialize(value); + } + } + + return @default; + } + + /// + /// Saves a group of items by its key in a composite. + /// This method should be considered for objects that do not exceed 8k bytes during the lifetime of the application + /// and for groups of settings which need to be treated in an atomic way. + /// + /// Type of object saved. + /// Key of the composite (that contains settings). + /// Objects to save. + public void Save(string compositeKey, IDictionary values) + { + if (TryRead(compositeKey, out ApplicationDataCompositeValue? composite) && composite != null) + { + foreach (KeyValuePair setting in values) + { + var serializedValue = Serializer.Serialize(setting.Value) ?? string.Empty; + if (composite.ContainsKey(setting.Key)) + { + composite[setting.Key] = serializedValue; + } + else + { + composite.Add(setting.Key, serializedValue); + } + } + } + else + { + composite = []; + foreach (KeyValuePair setting in values) + { + var serializedValue = Serializer.Serialize(setting.Value) ?? string.Empty; + composite.Add(setting.Key, serializedValue); + } + + Settings.Values[compositeKey] = composite; + } + } + + /// + /// Deletes a single item by its key in composite. + /// + /// Key of the composite (that contains settings). + /// Key of the object. + /// A boolean indicator of success. + public bool TryDelete(string compositeKey, string key) + { + if (TryRead(compositeKey, out ApplicationDataCompositeValue? composite) && composite != null) + { + return composite.Remove(key); + } + + return false; + } + + /// + public Task ReadFileAsync(string filePath, T? @default = default) + { + return ReadFileAsync(Folder, filePath, @default); + } + + /// + public Task> ReadFolderAsync(string folderPath) + { + return ReadFolderAsync(Folder, folderPath); + } + + /// + public Task CreateFileAsync(string filePath, T value) + { + return CreateFileAsync(Folder, filePath, value); + } + + /// + public Task CreateFolderAsync(string folderPath) + { + return CreateFolderAsync(Folder, folderPath); + } + + /// + public Task TryDeleteItemAsync(string itemPath) + { + return TryDeleteItemAsync(Folder, itemPath); + } + + /// + public Task TryRenameItemAsync(string itemPath, string newName) + { + return TryRenameItemAsync(Folder, itemPath, newName); + } + + private async Task ReadFileAsync(StorageFolder folder, string filePath, T? @default = default) + { + string value = await StorageFileHelper.ReadTextFromFileAsync(folder, NormalizePath(filePath)); + return (value != null) ? Serializer.Deserialize(value) : @default; + } + + private async Task> ReadFolderAsync(StorageFolder folder, string folderPath) + { + var targetFolder = await folder.GetFolderAsync(NormalizePath(folderPath)); + var items = await targetFolder.GetItemsAsync(); + + return items.Select((item) => + { + var itemType = item.IsOfType(StorageItemTypes.File) ? DirectoryItemType.File + : item.IsOfType(StorageItemTypes.Folder) ? DirectoryItemType.Folder + : DirectoryItemType.None; + + return (itemType, item.Name); + }); + } + + private async Task CreateFileAsync(StorageFolder folder, string filePath, T value) + { + var serializedValue = Serializer.Serialize(value)?.ToString() ?? string.Empty; + return await StorageFileHelper.WriteTextToFileAsync(folder, serializedValue, NormalizePath(filePath), CreationCollisionOption.ReplaceExisting); + } + + private async Task CreateFolderAsync(StorageFolder folder, string folderPath) + { + await folder.CreateFolderAsync(NormalizePath(folderPath), CreationCollisionOption.OpenIfExists); + } + + private async Task TryDeleteItemAsync(StorageFolder folder, string itemPath) + { + try + { + var item = await folder.GetItemAsync(NormalizePath(itemPath)); + await item.DeleteAsync(); + return true; + } + catch + { + return false; + } + } + + private async Task TryRenameItemAsync(StorageFolder folder, string itemPath, string newName) + { + try + { + var item = await folder.GetItemAsync(NormalizePath(itemPath)); + await item.RenameAsync(newName, NameCollisionOption.FailIfExists); + return true; + } + catch + { + return false; + } + } + + private string NormalizePath(string path) + { + var directoryName = Path.GetDirectoryName(path) ?? string.Empty; + var fileName = Path.GetFileName(path); + return Path.Combine(directoryName, fileName); + } +} From edb94116895d1deaff31b17cb99c3dba5cb98ee8 Mon Sep 17 00:00:00 2001 From: Lamparter <71598437+Lamparter@users.noreply.github.com> Date: Tue, 7 Jan 2025 18:50:47 +0000 Subject: [PATCH 3/4] Port `OSVersion` --- components/Helpers/src/OSVersion.cs | 38 +++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 components/Helpers/src/OSVersion.cs diff --git a/components/Helpers/src/OSVersion.cs b/components/Helpers/src/OSVersion.cs new file mode 100644 index 00000000..26a04627 --- /dev/null +++ b/components/Helpers/src/OSVersion.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Helpers; + +/// +/// Defines Operating System version +/// +public struct OSVersion +{ + /// + /// Value describing major version + /// + public ushort Major; + + /// + /// Value describing minor version + /// + public ushort Minor; + + /// + /// Value describing build + /// + public ushort Build; + + /// + /// Value describing revision + /// + public ushort Revision; + + /// + /// Converts OSVersion to string + /// + /// Major.Minor.Build.Revision as a string + public override readonly string ToString() + => $"{Major}.{Minor}.{Build}.{Revision}"; +} From 4778a9d6d6ea9a6fc4ffe1ccf53f60adb2ffe389 Mon Sep 17 00:00:00 2001 From: Lamparter <71598437+Lamparter@users.noreply.github.com> Date: Tue, 7 Jan 2025 18:51:07 +0000 Subject: [PATCH 4/4] Port `SystemInformation` --- components/Helpers/src/SystemInformation.cs | 435 ++++++++++++++++++++ 1 file changed, 435 insertions(+) create mode 100644 components/Helpers/src/SystemInformation.cs diff --git a/components/Helpers/src/SystemInformation.cs b/components/Helpers/src/SystemInformation.cs new file mode 100644 index 00000000..56891ad9 --- /dev/null +++ b/components/Helpers/src/SystemInformation.cs @@ -0,0 +1,435 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using CommunityToolkit.Common.Helpers; +using Microsoft.UI.Xaml; +using Windows.ApplicationModel; +using Windows.ApplicationModel.Activation; +using Windows.Security.ExchangeActiveSyncProvisioning; +using Windows.System; +using Windows.System.Profile; +using Windows.System.UserProfile; +using Windows.UI.Core; +using CommunityToolkit.WinUI.Helpers; +using CommunityToolkit.Common; +using CommunityToolkit.WinUI.Helpers.ObjectStorage; + +namespace CommunityToolkit.WinUI.Helpers; + +/// +/// This class provides info about the app and the system. +/// +public sealed class SystemInformation +{ + /// + /// The instance used to save and retrieve application settings. + /// + private readonly ApplicationDataStorageHelper _settingsStorage = ApplicationDataStorageHelper.GetCurrent(); + + /// + /// The starting time of the current application session (since app launch or last move to foreground). + /// + private DateTime _sessionStart; + + /// + /// Initializes a new instance of the class. + /// + private SystemInformation() + { + ApplicationName = Package.Current.DisplayName; + ApplicationVersion = Package.Current.Id.Version; + +#if !HAS_UNO + try + { + Culture = GlobalizationPreferences.Languages.Count > 0 ? new CultureInfo(GlobalizationPreferences.Languages[0]) : null; + } + catch + { + Culture = null; + } +#endif + + DeviceFamily = AnalyticsInfo.VersionInfo.DeviceFamily; + +#if !HAS_UNO + ulong version = ulong.Parse(AnalyticsInfo.VersionInfo.DeviceFamilyVersion); + + OperatingSystemVersion = new OSVersion + { + Major = (ushort)((version & 0xFFFF000000000000L) >> 48), + Minor = (ushort)((version & 0x0000FFFF00000000L) >> 32), + Build = (ushort)((version & 0x00000000FFFF0000L) >> 16), + Revision = (ushort)(version & 0x000000000000FFFFL) + }; + + OperatingSystemArchitecture = Package.Current.Id.Architecture; +#endif + + EasClientDeviceInformation deviceInfo = new(); + + OperatingSystem = deviceInfo.OperatingSystem; + DeviceManufacturer = deviceInfo.SystemManufacturer; + DeviceModel = deviceInfo.SystemProductName; + IsFirstRun = DetectIfFirstUse(); + (IsAppUpdated, PreviousVersionInstalled) = DetectIfAppUpdated(); + FirstUseTime = DetectFirstUseTime(); + FirstVersionInstalled = DetectFirstVersionInstalled(); + + InitializeValuesSetWithTrackAppUse(); + } + + /// + /// Gets the unique instance of . + /// + public static SystemInformation Instance { get; } = new(); + + /// + /// Gets the application's name. + /// + public string ApplicationName { get; } + + /// + /// Gets the application's version. + /// + public PackageVersion ApplicationVersion { get; } + +#if !HAS_UNO + /// + /// Gets the user's most preferred culture. + /// + public CultureInfo? Culture { get; } +#endif + + /// + /// Gets the device's family. + /// + /// Common values include: + /// + /// "Windows.Desktop" + /// "Windows.Mobile" + /// "Windows.Xbox" + /// "Windows.Holographic" + /// "Windows.Team" + /// "Windows.IoT" + /// + /// + /// Prepare your code for other values. + /// + public string DeviceFamily { get; } + + /// + /// Gets the operating system's name. + /// + public string OperatingSystem { get; } + +#if !HAS_UNO + /// + /// Gets the operating system's version. + /// + public OSVersion OperatingSystemVersion { get; } + + /// + /// Gets the processor architecture. + /// + public Windows.System.ProcessorArchitecture OperatingSystemArchitecture { get; } +#endif + + /// + /// Gets the available memory. + /// + public static float AvailableMemory => (float)MemoryManager.AppMemoryUsageLimit / 1024 / 1024; + + /// + /// Gets the device's model. + /// Will be empty if the model couldn't be determined (For example: when running in a virtual machine). + /// + public string DeviceModel { get; } + + /// + /// Gets the device's manufacturer. + /// Will be empty if the manufacturer couldn't be determined (For example: when running in a virtual machine). + /// + public string DeviceManufacturer { get; } + + /// + /// Gets a value indicating whether the app is being used for the first time since it was installed. + /// Use this to tell if you should do or display something different for the app's first use. + /// + public bool IsFirstRun { get; } + + /// + /// Gets a value indicating whether the app is being used for the first time since being upgraded from an older version. + /// Use this to tell if you should display details about what has changed. + /// + public bool IsAppUpdated { get; } + + /// + /// Gets the first version of the app that was installed. + /// This will be the current version if a previous version of the app was installed before accessing this property. + /// + public PackageVersion FirstVersionInstalled { get; } + + /// + /// Gets the previous version of the app that was installed. + /// This will be the current version if a previous version of the app was installed + /// before using or if the app is not updated. + /// + public PackageVersion PreviousVersionInstalled { get; } + + /// + /// Gets the DateTime (in UTC) when the app was launched for the first time. + /// + public DateTime FirstUseTime { get; } + + /// + /// Gets the DateTime (in UTC) when the app was last launched, not including this instance. + /// Will be if has not been called yet. + /// + public DateTime LastLaunchTime { get; private set; } + + /// + /// Gets the number of times the app has been launched. + /// Will be 0 if has not been called yet. + /// + public long LaunchCount { get; private set; } + + /// + /// Gets the number of times the app has been launched. + /// Will be 0 if has not been called yet. + /// + public long TotalLaunchCount { get; private set; } + + /// + /// Gets the DateTime (in UTC) that this instance of the app was launched. + /// Will be if has not been called yet. + /// + public DateTime LaunchTime { get; private set; } + + /// + /// Gets the DateTime (in UTC) when the launch count was last reset. + /// Will be if has not been called yet. + /// + public DateTime LastResetTime { get; private set; } + + /// + /// Gets the length of time this instance of the app has been running. + /// Will be if has not been called yet. + /// + public TimeSpan AppUptime + { + get + { + if (LaunchCount > 0) + { + var subSessionLength = DateTime.UtcNow.Subtract(_sessionStart).Ticks; + var uptimeSoFar = _settingsStorage.Read(nameof(AppUptime)); + + return new(uptimeSoFar + subSessionLength); + } + + return TimeSpan.MinValue; + } + } + + /// + /// Adds to the record of how long the app has been running. + /// Use this to optionally include time spent in background tasks or extended execution. + /// + /// The amount to time to add + public void AddToAppUptime(TimeSpan duration) + { + var uptimeSoFar = _settingsStorage.Read(nameof(AppUptime)); + + _settingsStorage.Save(nameof(AppUptime), uptimeSoFar + duration.Ticks); + } + + /// + /// Resets the launch count. + /// + public void ResetLaunchCount() + { + LastResetTime = DateTime.UtcNow; + LaunchCount = 0; + + _settingsStorage.Save(nameof(LastResetTime), LastResetTime.ToFileTimeUtc()); + _settingsStorage.Save(nameof(LaunchCount), LaunchCount); + } + + /// + /// Tracks information about the app's launch. + /// + /// Details about the launch request and process. + /// The XamlRoot object from your visual tree. + public void TrackAppUse(IActivatedEventArgs args, XamlRoot? xamlRoot = null) + { + if (args.PreviousExecutionState is ApplicationExecutionState.ClosedByUser or ApplicationExecutionState.NotRunning) + { + LaunchCount = _settingsStorage.Read(nameof(LaunchCount)) + 1; + TotalLaunchCount = _settingsStorage.Read(nameof(TotalLaunchCount)) + 1; + + // In case we upgraded the properties, make TotalLaunchCount is correct + if (TotalLaunchCount < LaunchCount) + { + TotalLaunchCount = LaunchCount; + } + + _settingsStorage.Save(nameof(LaunchCount), LaunchCount); + _settingsStorage.Save(nameof(TotalLaunchCount), TotalLaunchCount); + + LaunchTime = DateTime.UtcNow; + + var lastLaunch = _settingsStorage.Read(nameof(LastLaunchTime)); + + LastLaunchTime = lastLaunch != 0 + ? DateTime.FromFileTimeUtc(lastLaunch) + : LaunchTime; + + _settingsStorage.Save(nameof(LastLaunchTime), LaunchTime.ToFileTimeUtc()); + _settingsStorage.Save(nameof(AppUptime), 0L); + + var lastResetTime = _settingsStorage.Read(nameof(LastResetTime)); + + LastResetTime = lastResetTime != 0 + ? DateTime.FromFileTimeUtc(lastResetTime) + : DateTime.MinValue; + } + + if (xamlRoot != null) + { + void XamlRoot_Changed(XamlRoot sender, XamlRootChangedEventArgs e) + { + UpdateVisibility(sender.IsHostVisible); + } + + xamlRoot.Changed -= XamlRoot_Changed; + xamlRoot.Changed += XamlRoot_Changed; + } + else + { + void App_VisibilityChanged(CoreWindow sender, VisibilityChangedEventArgs e) + { + UpdateVisibility(e.Visible); + } + + var windowForCurrentThread = CoreWindow.GetForCurrentThread(); + if (windowForCurrentThread != null) + { + windowForCurrentThread.VisibilityChanged -= App_VisibilityChanged; + windowForCurrentThread.VisibilityChanged += App_VisibilityChanged; + } + } + } + + private void UpdateVisibility(bool visible) + { + if (visible) + { + _sessionStart = DateTime.UtcNow; + } + else + { + var subSessionLength = DateTime.UtcNow.Subtract(_sessionStart).Ticks; + var uptimeSoFar = _settingsStorage.Read(nameof(AppUptime)); + + _settingsStorage.Save(nameof(AppUptime), uptimeSoFar + subSessionLength); + } + } + + private bool DetectIfFirstUse() + { + if (_settingsStorage.KeyExists(nameof(IsFirstRun))) + { + return false; + } + + _settingsStorage.Save(nameof(IsFirstRun), true); + + return true; + } + + private (bool IsUpdated, PackageVersion PreviousVersion) DetectIfAppUpdated() + { + var currentVersion = ApplicationVersion.ToFormattedString(); + + // If the "currentVersion" key does not exist, it means that this is the first time this method + // is ever called. That is, this is either the first time the app has been launched, or the first + // time a previously existing app has run this method (or has run it after a new update of the app). + // In this case, save the current version and report the same version as previous version installed. + if (!_settingsStorage.KeyExists(nameof(currentVersion))) + { + _settingsStorage.Save(nameof(currentVersion), currentVersion); + } + else + { + var previousVersion = _settingsStorage.Read(nameof(currentVersion)); + + // There are two possible cases if the "currentVersion" key exists: + // 1) The previous version is different than the current one. This means that the application + // has been updated since the last time this method was called. We can overwrite the saved + // setting for "currentVersion" to bring that value up to date, and return its old value. + // 2) The previous version matches the current one: the app has just been reopened without updates. + // In this case we have nothing to do and just return the previous version installed to be the same. + if (currentVersion != previousVersion && previousVersion != null) + { + _settingsStorage.Save(nameof(currentVersion), currentVersion); + return (true, previousVersion.ToPackageVersion()); + } + } + + return (false, currentVersion.ToPackageVersion()); + } + + private DateTime DetectFirstUseTime() + { + if (_settingsStorage.KeyExists(nameof(FirstUseTime))) + { + var firstUse = _settingsStorage.Read(nameof(FirstUseTime)); + + return DateTime.FromFileTimeUtc(firstUse); + } + + DateTime utcNow = DateTime.UtcNow; + + _settingsStorage.Save(nameof(FirstUseTime), utcNow.ToFileTimeUtc()); + + return utcNow; + } + + private PackageVersion DetectFirstVersionInstalled() + { + var firstVersionInstalled = _settingsStorage.Read(nameof(FirstVersionInstalled)); + if (firstVersionInstalled != null) + { + return firstVersionInstalled.ToPackageVersion(); + } + + _settingsStorage.Save(nameof(FirstVersionInstalled), ApplicationVersion.ToFormattedString()); + + return ApplicationVersion; + } + + private void InitializeValuesSetWithTrackAppUse() + { + LaunchTime = DateTime.MinValue; + LaunchCount = 0; + TotalLaunchCount = 0; + LastLaunchTime = DateTime.MinValue; + LastResetTime = DateTime.MinValue; + } + + /// + /// Launches the store app so the user can leave a review. + /// + /// A representing the asynchronous operation. + /// This method needs to be called from your UI thread. + public static Task LaunchStoreForReviewAsync() + { + return Launcher.LaunchUriAsync(new Uri(string.Format("ms-windows-store://review/?PFN={0}", Package.Current.Id.FamilyName))).AsTask(); + } +}