Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/libraries/Common/src/Interop/Interop.TimeZoneInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ internal static partial class Interop
{
internal static partial class Globalization
{
[LibraryImport(Libraries.GlobalizationNative, EntryPoint = "GlobalizationNative_GetCanonicalLocationTimeZoneIds")]
internal static partial int GetCanonicalLocationTimeZoneIds([Out] char[]? value, int valueLength);

[LibraryImport(Libraries.GlobalizationNative, EntryPoint = "GlobalizationNative_GetTimeZoneDisplayName", StringMarshalling = StringMarshalling.Utf16)]
internal static unsafe partial ResultCode GetTimeZoneDisplayName(
string localeName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
Expand Down Expand Up @@ -211,6 +212,63 @@ private static string GetApexTimeDataRoot()
return "/apex/com.android.tzdata";
}

private static string? GetVersionedTimeZoneDataDirectory(string apexTimeDataRoot)
{
// Android 15+ stores time zone data under versioned/<format-major>.
// Use the newest format supported by the running OS; newer releases retain older formats for compatibility.
// https://android.googlesource.com/platform/system/timezone/+/0470df3d38d8e08932ebbe08b3d8ec9bbdcd403f/README.android
string? formatMajorVersion =
OperatingSystem.IsAndroidVersionAtLeast(36) ? "9" :
OperatingSystem.IsAndroidVersionAtLeast(35) ? "8" :
OperatingSystem.IsAndroidVersionAtLeast(34) ? "7" :
OperatingSystem.IsAndroidVersionAtLeast(33) ? "6" :
OperatingSystem.IsAndroidVersionAtLeast(31) ? "5" :
OperatingSystem.IsAndroidVersionAtLeast(30) ? "4" :
OperatingSystem.IsAndroidVersionAtLeast(29) ? "3" :
null;

if (formatMajorVersion is null)
{
return null;
}

string versionedRoot = Path.Combine(apexTimeDataRoot, "etc/tz/versioned");
string preferredDirectory = Path.Combine(versionedRoot, formatMajorVersion);
if (File.Exists(Path.Combine(preferredDirectory, TimeZoneFileName)))
{
return preferredDirectory;
}

// Compatibility versions are eventually pruned. If the preferred version is gone,
// use the newest available data rather than falling back to the non-updatable system copy.
string? latestDirectory = null;
int latestVersion = -1;
try
{
foreach (string directory in Directory.EnumerateDirectories(versionedRoot))
{
string directoryName = Path.GetFileName(directory);
if (int.TryParse(directoryName, NumberStyles.None, CultureInfo.InvariantCulture, out int version) &&
version > latestVersion &&
File.Exists(Path.Combine(directory, TimeZoneFileName)))
{
latestDirectory = directory;
latestVersion = version;
}
}
}
catch (IOException)
{
return null;
}
catch (UnauthorizedAccessException)
{
return null;
}

return latestDirectory;
}

private static string GetApexRuntimeRoot()
{
string? ret = Environment.GetEnvironmentVariable("ANDROID_RUNTIME_ROOT");
Expand All @@ -227,12 +285,19 @@ public AndroidTzData()
// On Android, time zone data is found in tzdata
// Based on https://github.com/mono/mono/blob/main/mcs/class/corlib/System/TimeZoneInfo.Android.cs
// Also follows the locations found at the bottom of https://github.com/aosp-mirror/platform_bionic/blob/master/libc/tzcode/bionic.cpp
ReadOnlySpan<string> tzFileDirList = [ GetApexTimeDataRoot() + "/etc/tz/", // Android 10+, TimeData module where the updates land
GetApexRuntimeRoot() + "/etc/tz/", // Android 10+, Fallback location if the above isn't found or corrupted
Environment.GetEnvironmentVariable("ANDROID_DATA") + "/misc/zoneinfo/",
Environment.GetEnvironmentVariable("ANDROID_ROOT") + DefaultTimeZoneDirectory ];
foreach (var tzFileDir in tzFileDirList)
string apexTimeDataRoot = GetApexTimeDataRoot();
ReadOnlySpan<string?> tzFileDirList = [ GetVersionedTimeZoneDataDirectory(apexTimeDataRoot), // Android 15+, versioned TimeData module
apexTimeDataRoot + "/etc/tz/", // Android 10+, unversioned TimeData module
GetApexRuntimeRoot() + "/etc/tz/", // Android 10+, fallback location if the above isn't found or corrupted
Environment.GetEnvironmentVariable("ANDROID_DATA") + "/misc/zoneinfo/",
Environment.GetEnvironmentVariable("ANDROID_ROOT") + DefaultTimeZoneDirectory ];
foreach (string? tzFileDir in tzFileDirList)
{
if (string.IsNullOrEmpty(tzFileDir))
{
continue;
}

string tzFilePath = Path.Combine(tzFileDir, TimeZoneFileName);
if (LoadData(tzFileDir, tzFilePath))
{
Expand Down Expand Up @@ -272,7 +337,9 @@ public AndroidTzData()
{
string tzLookupFilePath = Path.Combine(tzFileDir, "tzlookup.xml");
if (!File.Exists(tzLookupFilePath))
return null;
{
return GetCanonicalLocationTimeZoneIds();
}

HashSet<string>? tzLookupIDs = null;
try
Expand Down Expand Up @@ -300,12 +367,52 @@ public AndroidTzData()
}
}
}
catch
catch (IOException)
{
return GetCanonicalLocationTimeZoneIds();
}
catch (UnauthorizedAccessException)
{
return GetCanonicalLocationTimeZoneIds();
}

return tzLookupIDs ?? GetCanonicalLocationTimeZoneIds();
}

private static HashSet<string>? GetCanonicalLocationTimeZoneIds()
{
if (GlobalizationMode.Invariant)
{
return null;
}

int bufferLength = Interop.Globalization.GetCanonicalLocationTimeZoneIds(null, 0);
if (bufferLength <= 0)
{
return null;
}

return tzLookupIDs;
char[] buffer = new char[bufferLength];
if (Interop.Globalization.GetCanonicalLocationTimeZoneIds(buffer, bufferLength) != bufferLength)
{
return null;
}

HashSet<string> ids = new HashSet<string>();
int index = 0;
while (index < bufferLength)
{
int idLength = buffer[index++];
if (idLength == 0 || idLength > bufferLength - index)
{
return null;
}

ids.Add(new string(buffer, index, idLength));
index += idLength;
}

return ids;
Comment on lines +395 to +415
}

[MemberNotNullWhen(true, nameof(_ids))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3156,14 +3156,8 @@ public static void FijiTimeZoneTest()
}

[ConditionalFact]
[ActiveIssue("https://github.com/dotnet/runtime/issues/117731", TestPlatforms.Android)]
public static void NoBackwardTimeZones()
{
if (OperatingSystem.IsAndroid() && !OperatingSystem.IsAndroidVersionAtLeast(26))
{
throw new SkipTestException("This test won't work on API level < 26");
}

// Clear cached data to always ensure predictable results
TimeZoneInfo.ClearCachedData();
if (SupportLegacyTimeZoneNames)
Expand Down Expand Up @@ -3197,7 +3191,6 @@ public static void NoBackwardTimeZones()
}

[Fact]
[ActiveIssue("https://github.com/dotnet/runtime/issues/90269", TestPlatforms.Android)]
public static void TestGetSystemTimeZones()
{
TimeZoneInfo.ClearCachedData(); // Start clean
Expand Down
1 change: 1 addition & 0 deletions src/native/libs/System.Globalization.Native/entrypoints.c
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ static const Entry s_globalizationNative[] =
DllImportEntry(GlobalizationNative_NormalizeString)
DllImportEntry(GlobalizationNative_StartsWith)
#ifndef __wasm__
DllImportEntry(GlobalizationNative_GetCanonicalLocationTimeZoneIds)
DllImportEntry(GlobalizationNative_GetTimeZoneDisplayName)
DllImportEntry(GlobalizationNative_IanaIdToWindowsId)
DllImportEntry(GlobalizationNative_WindowsIdToIanaId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,13 @@ int32_t GlobalizationNative_NormalizeString(
}

// Placeholder for time zone data
int32_t GlobalizationNative_GetCanonicalLocationTimeZoneIds(
UChar* value, int32_t valueLength)
{
assert_msg(false, "Not supported on this platform", 0);
return 0;
}
Comment on lines +251 to +256

int32_t GlobalizationNative_WindowsIdToIanaId(
const UChar* windowsId, const char* region, UChar* ianaId, int32_t ianaIdLength)
{
Expand Down
60 changes: 60 additions & 0 deletions src/native/libs/System.Globalization.Native/pal_timeZoneInfo.c
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,66 @@ int32_t GlobalizationNative_IanaIdToWindowsId(const UChar* ianaId, UChar* window
return 0;
}

/*
Get canonical location time zone IDs from ICU.
If value is null, returns the length needed to store the IDs.
Otherwise, stores each ID prefixed by its length and returns the number of UChars written.
Returns a negative number on failure.
*/
int32_t GlobalizationNative_GetCanonicalLocationTimeZoneIds(UChar* value, int32_t valueLength)
{
if (valueLength < 0)
{
return -1;
}

UErrorCode status = U_ZERO_ERROR;
UEnumeration* timeZoneIds = ucal_openTimeZoneIDEnumeration(UCAL_ZONE_TYPE_CANONICAL_LOCATION, NULL, NULL, &status);
if (U_FAILURE(status) || timeZoneIds == NULL)
{
if (timeZoneIds != NULL)
{
uenum_close(timeZoneIds);
}

return -1;
}

int32_t totalLength = 0;
int32_t index = 0;
int32_t timeZoneIdLength;
const char* timeZoneId;

while ((timeZoneId = uenum_next(timeZoneIds, &timeZoneIdLength, &status)) != NULL)
{
if (U_FAILURE(status) ||
timeZoneIdLength <= 0 ||
timeZoneIdLength > UINT16_MAX ||
totalLength > INT32_MAX - timeZoneIdLength - 1)
{
uenum_close(timeZoneIds);
return -2;
}

totalLength += timeZoneIdLength + 1;
if (value != NULL)
{
if (totalLength > valueLength)
{
uenum_close(timeZoneIds);
return -3;
}

value[index++] = (UChar)timeZoneIdLength;
u_charsToUChars(timeZoneId, value + index, timeZoneIdLength);
index += timeZoneIdLength;
}
}

uenum_close(timeZoneIds);
return U_SUCCESS(status) ? totalLength : -2;
}

/*
Private function to get the standard and daylight names from the ICU Calendar API.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ typedef enum
TimeZoneDisplayName_ExemplarCity = 4,
TimeZoneDisplayName_TimeZoneName = 5,
} TimeZoneDisplayNameType;
PALEXPORT int32_t GlobalizationNative_GetCanonicalLocationTimeZoneIds(UChar* value, int32_t valueLength);
PALEXPORT int32_t GlobalizationNative_WindowsIdToIanaId(const UChar* windowsId, const char* region, UChar* ianaId, int32_t ianaIdLength);
PALEXPORT int32_t GlobalizationNative_IanaIdToWindowsId(const UChar* ianaId, UChar* windowsId, int32_t windowsIdLength);
PALEXPORT ResultCode GlobalizationNative_GetTimeZoneDisplayName(const UChar* localeName, const UChar* timeZoneId, TimeZoneDisplayNameType type, UChar* result, int32_t resultLength);
Expand Down
Loading