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 a908b752f45f4..e4d61cc09464a 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..c40e4708e47aa
--- /dev/null
+++ b/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.Android.cs
@@ -0,0 +1,73 @@
+// 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
+ {
+ // 0 == in process of being loaded, 1 == loaded
+ private static volatile int s_androidTZDataLoaded = -1;
+
+ // 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 == 1) // The background thread finished, the cache is loaded.
+ {
+ return ToLocalTime(utcDateTime, true);
+ }
+
+ object? localDateTimeOffset = AppContext.GetData("System.TimeZoneInfo.LocalDateTimeOffset");
+ if (localDateTimeOffset == null) // If no offset property provided through monovm app context, default
+ {
+ // no need to create the thread, load tzdata now
+ s_androidTZDataLoaded = 1;
+ return ToLocalTime(utcDateTime, true);
+ }
+
+ // The cache isn't loaded yet.
+ if (Interlocked.CompareExchange(ref s_androidTZDataLoaded, 0, -1) == -1)
+ {
+ new Thread(() =>
+ {
+ try
+ {
+ // 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
+ }
+ finally
+ {
+ s_androidTZDataLoaded = 1;
+ }
+ }) { IsBackground = true }.Start();
+ }
+
+ // 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..6a32968ac9ab9 100644
--- a/src/tasks/AndroidAppBuilder/Templates/MonoRunner.java
+++ b/src/tasks/AndroidAppBuilder/Templates/MonoRunner.java
@@ -24,8 +24,11 @@
import java.io.OutputStream;
import java.io.BufferedInputStream;
import java.util.ArrayList;
+import java.util.Calendar;
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 +91,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 = getLocalDateTimeOffset();
+ return initRuntime(filesDir, cacheDir, testResultsDir, entryPointLibName, args, localDateTimeOffset);
}
@Override
@@ -149,7 +153,17 @@ static void unzipAssets(Context context, String toPath, String zipName) {
}
}
- static native int initRuntime(String libsDir, String cacheDir, String testResultsDir, String entryPointLibName, String[] args);
+ static int getLocalDateTimeOffset() {
+ if (android.os.Build.VERSION.SDK_INT >= 26) {
+ return OffsetDateTime.now().getOffset().getTotalSeconds();
+ }
+ else {
+ int offsetInMillis = Calendar.getInstance().getTimeZone().getRawOffset();
+ return offsetInMillis / 1000;
+ }
+ }
+
+ 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)
{