diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
index 86bde088f4ad7..c334d0a1dbd90 100644
--- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
+++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
@@ -254,6 +254,8 @@
+
+
diff --git a/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.Android.cs b/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.Android.cs
new file mode 100644
index 0000000000000..c78dadcdfc3bd
--- /dev/null
+++ b/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.Android.cs
@@ -0,0 +1,89 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Threading;
+
+namespace System
+{
+ public readonly partial struct DateTimeOffset
+ {
+ private static bool s_androidTZDataLoaded;
+ private static readonly object s_localUtcOffsetLock = new();
+ private static Thread? s_loadAndroidTZData;
+ private static bool s_startNewBackgroundThread = true;
+
+ // Now on Android does the following
+ // 1) quickly returning a fast path result when first called if the right AppContext data element is set
+ // 2) starting a background thread to load TimeZoneInfo local cache
+ //
+ // On Android, loading AndroidTZData is expensive for startup performance.
+ // The fast result relies on `System.TimeZoneInfo.LocalDateTimeOffset` being set
+ // in the App Context's properties as the appropriate local date time offset from UTC.
+ // monovm_initialize(_preparsed) can be leveraged to do so.
+ // However, to handle timezone changes during the app lifetime, AndroidTZData needs to be loaded.
+ // So, on first call, we return the fast path and start a background thread to load
+ // the TimeZoneInfo Local cache implementation which loads AndroidTZData.
+ public static DateTimeOffset Now
+ {
+ get
+ {
+ DateTime utcDateTime = DateTime.UtcNow;
+
+ if (s_androidTZDataLoaded) // The background thread finished, the cache is loaded.
+ return ToLocalTime(utcDateTime, true);
+
+ if (s_startNewBackgroundThread) // The cache isn't loaded and no background thread has been created
+ {
+ lock (s_localUtcOffsetLock)
+ {
+ // Now may be called multiple times before a cache is loaded and a background thread is running,
+ // once the lock is available, check for a cache and background thread.
+ if (s_androidTZDataLoaded)
+ return ToLocalTime(utcDateTime, true);
+
+ if (s_loadAndroidTZData == null)
+ {
+ s_loadAndroidTZData = new Thread(() => {
+ // Delay the background thread to avoid impacting startup, if it still coincides after 1s, startup is already perceived as slow
+ Thread.Sleep(1000);
+
+ _ = TimeZoneInfo.Local; // Load AndroidTZData
+ s_androidTZDataLoaded = true;
+
+ lock (s_localUtcOffsetLock)
+ {
+ s_loadAndroidTZData = null; // Ensure thread is cleared when cache is loaded
+ }
+ });
+ s_loadAndroidTZData.IsBackground = true;
+ }
+ }
+
+ if (s_startNewBackgroundThread)
+ {
+ // Because Start does not block the calling thread,
+ // setting the boolean flag to false immediately after should
+ // prevent two calls to DateTimeOffset.Now in quick succession
+ // from both reaching here.
+ s_loadAndroidTZData.Start();
+ s_startNewBackgroundThread = false;
+ }
+ }
+
+
+ object? localDateTimeOffset = AppContext.GetData("System.TimeZoneInfo.LocalDateTimeOffset");
+ if (localDateTimeOffset == null) // If no offset property provided through monovm app context, default
+ return ToLocalTime(utcDateTime, true);
+
+ // Fast path obtained offset incorporated into ToLocalTime(DateTime.UtcNow, true) logic
+ int localDateTimeOffsetSeconds = Convert.ToInt32(localDateTimeOffset);
+ TimeSpan offset = TimeSpan.FromSeconds(localDateTimeOffsetSeconds);
+ long localTicks = utcDateTime.Ticks + offset.Ticks;
+ if (localTicks < DateTime.MinTicks || localTicks > DateTime.MaxTicks)
+ throw new ArgumentException(SR.Arg_ArgumentOutOfRangeException);
+
+ return new DateTimeOffset(localTicks, offset);
+ }
+ }
+ }
+}
diff --git a/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.NonAndroid.cs b/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.NonAndroid.cs
new file mode 100644
index 0000000000000..511e40ef6c8d1
--- /dev/null
+++ b/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.NonAndroid.cs
@@ -0,0 +1,12 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System
+{
+ public readonly partial struct DateTimeOffset
+ {
+ // Returns a DateTimeOffset representing the current date and time. The
+ // resolution of the returned value depends on the system timer.
+ public static DateTimeOffset Now => ToLocalTime(DateTime.UtcNow, true);
+ }
+}
diff --git a/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.cs b/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.cs
index 3f8feead885a7..b2289d1377ace 100644
--- a/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.cs
@@ -34,7 +34,7 @@ namespace System
[StructLayout(LayoutKind.Auto)]
[Serializable]
[TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")]
- public readonly struct DateTimeOffset
+ public readonly partial struct DateTimeOffset
: IComparable,
ISpanFormattable,
IComparable,
@@ -321,10 +321,6 @@ public DateTimeOffset(int year, int month, int day, int hour, int minute, int se
_dateTime = _dateTime.AddMicroseconds(microsecond);
}
- // Returns a DateTimeOffset representing the current date and time. The
- // resolution of the returned value depends on the system timer.
- public static DateTimeOffset Now => ToLocalTime(DateTime.UtcNow, true);
-
public static DateTimeOffset UtcNow
{
get
diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs
index e03c67beaa58d..d9b46074b8e35 100644
--- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs
@@ -107,7 +107,7 @@ private static int ParseGMTNumericZone(string name)
{
return new TimeZoneInfo(id, TimeSpan.FromSeconds(0), id, name, name, null, disableDaylightSavingTime:true);
}
- if (name.StartsWith("GMT", StringComparison.Ordinal))
+ if (name.Length >= 3 && name[0] == 'G' && name[1] == 'M' && name[2] == 'T')
{
return new TimeZoneInfo(id, TimeSpan.FromSeconds(ParseGMTNumericZone(name)), id, name, name, null, disableDaylightSavingTime:true);
}
diff --git a/src/tasks/AndroidAppBuilder/Templates/MonoRunner.java b/src/tasks/AndroidAppBuilder/Templates/MonoRunner.java
index 700d95e123f07..4bd5a68d41623 100644
--- a/src/tasks/AndroidAppBuilder/Templates/MonoRunner.java
+++ b/src/tasks/AndroidAppBuilder/Templates/MonoRunner.java
@@ -26,6 +26,8 @@
import java.util.ArrayList;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
public class MonoRunner extends Instrumentation
{
@@ -88,7 +90,8 @@ public static int initialize(String entryPointLibName, String[] args, Context co
unzipAssets(context, filesDir, "assets.zip");
Log.i("DOTNET", "MonoRunner initialize,, entryPointLibName=" + entryPointLibName);
- return initRuntime(filesDir, cacheDir, testResultsDir, entryPointLibName, args);
+ int localDateTimeOffset = OffsetDateTime.now().getOffset().getTotalSeconds();
+ return initRuntime(filesDir, cacheDir, testResultsDir, entryPointLibName, args, localDateTimeOffset);
}
@Override
@@ -149,7 +152,7 @@ static void unzipAssets(Context context, String toPath, String zipName) {
}
}
- static native int initRuntime(String libsDir, String cacheDir, String testResultsDir, String entryPointLibName, String[] args);
+ static native int initRuntime(String libsDir, String cacheDir, String testResultsDir, String entryPointLibName, String[] args, int local_date_time_offset);
static native int setEnv(String key, String value);
}
diff --git a/src/tasks/AndroidAppBuilder/Templates/monodroid.c b/src/tasks/AndroidAppBuilder/Templates/monodroid.c
index 1d8d3f1bd529c..a016353a0b4f3 100644
--- a/src/tasks/AndroidAppBuilder/Templates/monodroid.c
+++ b/src/tasks/AndroidAppBuilder/Templates/monodroid.c
@@ -203,7 +203,7 @@ cleanup_runtime_config (MonovmRuntimeConfigArguments *args, void *user_data)
}
int
-mono_droid_runtime_init (const char* executable, int managed_argc, char* managed_argv[])
+mono_droid_runtime_init (const char* executable, int managed_argc, char* managed_argv[], int local_date_time_offset)
{
// NOTE: these options can be set via command line args for adb or xharness, see AndroidSampleApp.csproj
@@ -225,13 +225,17 @@ mono_droid_runtime_init (const char* executable, int managed_argc, char* managed
// TODO: set TRUSTED_PLATFORM_ASSEMBLIES, APP_PATHS and NATIVE_DLL_SEARCH_DIRECTORIES
- const char* appctx_keys[2];
+ const char* appctx_keys[3];
appctx_keys[0] = "RUNTIME_IDENTIFIER";
appctx_keys[1] = "APP_CONTEXT_BASE_DIRECTORY";
+ appctx_keys[2] = "System.TimeZoneInfo.LocalDateTimeOffset";
- const char* appctx_values[2];
+ const char* appctx_values[3];
appctx_values[0] = ANDROID_RUNTIME_IDENTIFIER;
appctx_values[1] = bundle_path;
+ char local_date_time_offset_buffer[32];
+ snprintf (local_date_time_offset_buffer, sizeof(local_date_time_offset_buffer), "%d", local_date_time_offset);
+ appctx_values[2] = strdup (local_date_time_offset_buffer);
char *file_name = RUNTIMECONFIG_BIN_FILE;
int str_len = strlen (bundle_path) + strlen (file_name) + 1; // +1 is for the "/"
@@ -251,7 +255,7 @@ mono_droid_runtime_init (const char* executable, int managed_argc, char* managed
free (file_path);
}
- monovm_initialize(2, appctx_keys, appctx_values);
+ monovm_initialize(3, appctx_keys, appctx_values);
mono_debug_init (MONO_DEBUG_FORMAT_MONO);
mono_install_assembly_preload_hook (mono_droid_assembly_preload_hook, NULL);
@@ -318,7 +322,7 @@ Java_net_dot_MonoRunner_setEnv (JNIEnv* env, jobject thiz, jstring j_key, jstrin
}
int
-Java_net_dot_MonoRunner_initRuntime (JNIEnv* env, jobject thiz, jstring j_files_dir, jstring j_cache_dir, jstring j_testresults_dir, jstring j_entryPointLibName, jobjectArray j_args)
+Java_net_dot_MonoRunner_initRuntime (JNIEnv* env, jobject thiz, jstring j_files_dir, jstring j_cache_dir, jstring j_testresults_dir, jstring j_entryPointLibName, jobjectArray j_args, long current_local_time)
{
char file_dir[2048];
char cache_dir[2048];
@@ -347,7 +351,7 @@ Java_net_dot_MonoRunner_initRuntime (JNIEnv* env, jobject thiz, jstring j_files_
managed_argv[i + 1] = (*env)->GetStringUTFChars(env, j_arg, NULL);
}
- int res = mono_droid_runtime_init (executable, managed_argc, managed_argv);
+ int res = mono_droid_runtime_init (executable, managed_argc, managed_argv, current_local_time);
for (int i = 0; i < args_len; ++i)
{