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) {