From 3fc7ea2662a16eb7824f955921b5e7449efa33b7 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Mon, 27 Jul 2026 12:10:19 -0500 Subject: [PATCH 1/2] [PROTOTYPE] Remove Android time-zone change monitoring .NET for Android is the only .NET platform that watches for system time-zone changes and invalidates managed caches. dotnet/runtime has twice declined to do this (dotnet/runtime#31043, dotnet/runtime#97010), on the grounds that listening for the OS event should not be a cost imposed on every app. Verified on an emulator (API 36): * MonoVM/net10.0-android: the existing mechanism works, updating `TimeZoneInfo.Local` live. (`CultureInfo.ClearCachedData()` transitively calls `TimeZoneInfo.ClearCachedData()`.) * CoreCLR/net11.0-android: never updates -- `host-jni.cc` is `// TODO: implement or remove`. NativeAOT shares that host. * MonoVM is not buildable at all on `net11.0-android` (`NETSDK1242`), so the feature only functions in a configuration that can no longer be targeted. So this deletes the whole stack: the `NotifyTimeZoneChanges` Java receiver and its registration in both `MonoPackageManager.java` variants, the `notifyTimeZoneChanged` JNI entry point (Java declaration, generated header, both `.map.txt` export lists, the MonoVM `timezones.cc` implementation and the CoreCLR stub), the managed `AndroidEnvironment.NotifyTimeZoneChanged()` and its ILLink preserve entry. `PreserveTest` is retargeted at `Java.Lang.Object.SetHandleOnDeserialized`, which is still in the preserve list. Apps that need this behaviour can do it themselves in a few lines:: [BroadcastReceiver (Enabled = true, Exported = false)] [IntentFilter (new [] { Intent.ActionTimezoneChanged })] public class TimeZoneChangedReceiver : BroadcastReceiver { public override void OnReceive (Context? context, Intent? intent) => TimeZoneInfo.ClearCachedData (); } `ACTION_TIMEZONE_CHANGED` is on Android's implicit-broadcast exception list, so manifest declaration is allowed; it is also a protected system broadcast, so `RegisterReceiver()` needs no API-34 export flag. Both forms were verified on device. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6e170150-3891-4cdc-afa9-b8f9d62f1cfd --- .../PreserveLists/Mono.Android.xml | 3 - .../Android.Runtime/AndroidEnvironment.cs | 18 ----- .../java/mono/android/MonoPackageManager.java | 5 -- .../java/mono/android/Runtime.java | 1 - .../android/app/NotifyTimeZoneChanges.java | 8 --- .../mono/android/clr/MonoPackageManager.java | 5 -- src/native/clr/host/host-jni.cc | 6 -- src/native/clr/include/host/host-jni.hh | 7 -- src/native/clr/libnet-android.map.txt | 1 - src/native/mono/libmono-android.map.txt | 1 - src/native/mono/monodroid/CMakeLists.txt | 1 - .../mono/monodroid/mono_android_Runtime.h | 8 --- src/native/mono/monodroid/timezones.cc | 68 ------------------- .../Resources/LinkDescTest/PreserveTest.cs | 11 ++- 14 files changed, 5 insertions(+), 138 deletions(-) delete mode 100644 src/java-runtime/java/mono/android/app/NotifyTimeZoneChanges.java delete mode 100644 src/native/mono/monodroid/timezones.cc diff --git a/src/Microsoft.Android.Sdk.ILLink/PreserveLists/Mono.Android.xml b/src/Microsoft.Android.Sdk.ILLink/PreserveLists/Mono.Android.xml index 124edd61d93..b16215d73a3 100644 --- a/src/Microsoft.Android.Sdk.ILLink/PreserveLists/Mono.Android.xml +++ b/src/Microsoft.Android.Sdk.ILLink/PreserveLists/Mono.Android.xml @@ -2,9 +2,6 @@ - - - diff --git a/src/Mono.Android/Android.Runtime/AndroidEnvironment.cs b/src/Mono.Android/Android.Runtime/AndroidEnvironment.cs index cd487e8e3e4..2451bb77812 100644 --- a/src/Mono.Android/Android.Runtime/AndroidEnvironment.cs +++ b/src/Mono.Android/Android.Runtime/AndroidEnvironment.cs @@ -218,24 +218,6 @@ static Java.Security.Cert.X509Certificate ConvertCertificate (Java.Security.Cert .JavaCast()!; } - // This is invoked by libmonodroid.so. - // DO NOT REMOVE - static void NotifyTimeZoneChanged () - { - var thread = Thread.CurrentThread; - var timeZoneClearInfo = new[]{ - new { Description = "Thread.CurrentCulture.ClearCachedData()", Method = (Action) thread.CurrentCulture.ClearCachedData }, - new { Description = "Thread.CurrentUICulture.ClearCachedData()", Method = (Action) thread.CurrentUICulture.ClearCachedData }, - }; - foreach (var clearInfo in timeZoneClearInfo) { - try { - clearInfo.Method (); - } catch (Exception e) { - Logger.Log (LogLevel.Warn, "MonoAndroid", FormattableString.Invariant ($"Ignoring exception from {clearInfo.Description}: {e}")); - } - } - } - static void DetectCPUAndArchitecture (out ushort builtForCPU, out ushort runningOnCPU, out bool is64bit) { ushort built_for_cpu = 0; diff --git a/src/java-runtime/java/mono/android/MonoPackageManager.java b/src/java-runtime/java/mono/android/MonoPackageManager.java index 6531bd2da3d..f3e83a74ea6 100644 --- a/src/java-runtime/java/mono/android/MonoPackageManager.java +++ b/src/java-runtime/java/mono/android/MonoPackageManager.java @@ -43,11 +43,6 @@ public static void LoadApplication (Context context) ApplicationRegistration.Context = context; } if (!initialized) { - android.content.IntentFilter timezoneChangedFilter = new android.content.IntentFilter ( - android.content.Intent.ACTION_TIMEZONE_CHANGED - ); - context.registerReceiver (new mono.android.app.NotifyTimeZoneChanges (), timezoneChangedFilter); - Locale locale = Locale.getDefault (); String language = locale.getLanguage () + "-" + locale.getCountry (); String filesDir = context.getFilesDir ().getAbsolutePath (); diff --git a/src/java-runtime/java/mono/android/Runtime.java b/src/java-runtime/java/mono/android/Runtime.java index d1fa431814d..b3d566b2c6d 100644 --- a/src/java-runtime/java/mono/android/Runtime.java +++ b/src/java-runtime/java/mono/android/Runtime.java @@ -29,7 +29,6 @@ public static native void initInternal ( ); public static native void register (String managedType, java.lang.Class nativeClass, String methods); public static native void registerNatives (java.lang.Class nativeClass); - public static native void notifyTimeZoneChanged (); public static native int createNewContext (String[] runtimeApks, String[] assemblies, ClassLoader loader); public static native int createNewContextWithData (String[] runtimeApks, String[] assemblies, byte[][] assembliesBytes, String[] assembliesPaths, ClassLoader loader, boolean forcePreloadAssemblies); public static native void switchToContext (int contextID); diff --git a/src/java-runtime/java/mono/android/app/NotifyTimeZoneChanges.java b/src/java-runtime/java/mono/android/app/NotifyTimeZoneChanges.java deleted file mode 100644 index 21896a0f130..00000000000 --- a/src/java-runtime/java/mono/android/app/NotifyTimeZoneChanges.java +++ /dev/null @@ -1,8 +0,0 @@ -package mono.android.app; - -public class NotifyTimeZoneChanges extends android.content.BroadcastReceiver { - @Override - public void onReceive (android.content.Context context, android.content.Intent intent) { - mono.android.Runtime.notifyTimeZoneChanged (); - } -} diff --git a/src/java-runtime/java/mono/android/clr/MonoPackageManager.java b/src/java-runtime/java/mono/android/clr/MonoPackageManager.java index 5592cc2b68e..6db67d86fa8 100644 --- a/src/java-runtime/java/mono/android/clr/MonoPackageManager.java +++ b/src/java-runtime/java/mono/android/clr/MonoPackageManager.java @@ -43,11 +43,6 @@ public static void LoadApplication (Context context) ApplicationRegistration.Context = context; } if (!initialized) { - android.content.IntentFilter timezoneChangedFilter = new android.content.IntentFilter ( - android.content.Intent.ACTION_TIMEZONE_CHANGED - ); - context.registerReceiver (new mono.android.app.NotifyTimeZoneChanges (), timezoneChangedFilter); - Locale locale = Locale.getDefault (); String language = locale.getLanguage () + "-" + locale.getCountry (); String filesDir = context.getFilesDir ().getAbsolutePath (); diff --git a/src/native/clr/host/host-jni.cc b/src/native/clr/host/host-jni.cc index a41c1c507cc..fce770e1bbe 100644 --- a/src/native/clr/host/host-jni.cc +++ b/src/native/clr/host/host-jni.cc @@ -51,9 +51,3 @@ JNICALL Java_mono_android_Runtime_propagateUncaughtException (JNIEnv *env, [[may { Host::propagate_uncaught_exception (env, javaThread, javaException); } - -JNIEXPORT void -JNICALL Java_mono_android_Runtime_notifyTimeZoneChanged ([[maybe_unused]] JNIEnv *env, [[maybe_unused]] jclass klass) -{ - // TODO: implement or remove -} diff --git a/src/native/clr/include/host/host-jni.hh b/src/native/clr/include/host/host-jni.hh index 4904644ebd8..6bb3801cb4a 100644 --- a/src/native/clr/include/host/host-jni.hh +++ b/src/native/clr/include/host/host-jni.hh @@ -24,13 +24,6 @@ extern "C" { */ JNIEXPORT void JNICALL Java_mono_android_Runtime_initInternal (JNIEnv *, jclass, jstring, jobjectArray, jstring, jobjectArray, jint, jobject, jobjectArray, jboolean, jboolean); - /* - * Class: mono_android_Runtime - * Method: notifyTimeZoneChanged - * Signature: ()V - */ - JNIEXPORT void JNICALL Java_mono_android_Runtime_notifyTimeZoneChanged (JNIEnv *, jclass); - /* * Class: mono_android_Runtime * Method: propagateUncaughtException diff --git a/src/native/clr/libnet-android.map.txt b/src/native/clr/libnet-android.map.txt index 9c8a580bc34..39298f184ae 100644 --- a/src/native/clr/libnet-android.map.txt +++ b/src/native/clr/libnet-android.map.txt @@ -3,7 +3,6 @@ LIBNET_ANDROID { JNI_OnLoad; Java_mono_android_Runtime_dumpTimingData; Java_mono_android_Runtime_initInternal; - Java_mono_android_Runtime_notifyTimeZoneChanged; Java_mono_android_Runtime_propagateUncaughtException; Java_mono_android_Runtime_register; diff --git a/src/native/mono/libmono-android.map.txt b/src/native/mono/libmono-android.map.txt index a3b2c7ccb57..562a67869a3 100644 --- a/src/native/mono/libmono-android.map.txt +++ b/src/native/mono/libmono-android.map.txt @@ -4,7 +4,6 @@ LIBMONO_ANDROID { Java_mono_android_Runtime_dumpTimingData; Java_mono_android_Runtime_init; Java_mono_android_Runtime_initInternal; - Java_mono_android_Runtime_notifyTimeZoneChanged; Java_mono_android_Runtime_propagateUncaughtException; Java_mono_android_Runtime_register; diff --git a/src/native/mono/monodroid/CMakeLists.txt b/src/native/mono/monodroid/CMakeLists.txt index 43da300d837..ca48f41f06f 100644 --- a/src/native/mono/monodroid/CMakeLists.txt +++ b/src/native/mono/monodroid/CMakeLists.txt @@ -105,7 +105,6 @@ set(XAMARIN_MONODROID_SOURCES osbridge.cc runtime-environment.cc runtime-util.cc - timezones.cc ) list(APPEND LOCAL_CLANG_CHECK_SOURCES diff --git a/src/native/mono/monodroid/mono_android_Runtime.h b/src/native/mono/monodroid/mono_android_Runtime.h index 663a02a9b4f..1d761474006 100644 --- a/src/native/mono/monodroid/mono_android_Runtime.h +++ b/src/native/mono/monodroid/mono_android_Runtime.h @@ -31,14 +31,6 @@ JNIEXPORT void JNICALL Java_mono_android_Runtime_initInternal JNIEXPORT void JNICALL Java_mono_android_Runtime_register (JNIEnv *, jclass, jstring, jclass, jstring); -/* - * Class: mono_android_Runtime - * Method: notifyTimeZoneChanged - * Signature: ()V - */ -JNIEXPORT void JNICALL Java_mono_android_Runtime_notifyTimeZoneChanged - (JNIEnv *, jclass); - /* * Class: mono_android_Runtime * Method: createNewContext diff --git a/src/native/mono/monodroid/timezones.cc b/src/native/mono/monodroid/timezones.cc deleted file mode 100644 index 5816dd4ddce..00000000000 --- a/src/native/mono/monodroid/timezones.cc +++ /dev/null @@ -1,68 +0,0 @@ -#include -#include -#include - -#include -#include -#include -#include -#include - -#include "java-interop-util.h" - -#include "mono_android_Runtime.h" -#include "monodroid.h" -#include "debug.hh" -#include "embedded-assemblies.hh" -#include "util.hh" -#include "monodroid-glue.hh" -#include "globals.hh" - -using namespace xamarin::android; -using namespace xamarin::android::internal; - -static MonoMethod *AndroidEnvironment_NotifyTimeZoneChanged; - -static void -init () -{ - MonoAssembly *Mono_Android_dll; - MonoImage *Mono_Android_image; - MonoClass *AndroidEnvironment; - - if (AndroidEnvironment_NotifyTimeZoneChanged) - return; - - Mono_Android_dll = Util::monodroid_load_assembly (Util::get_current_domain (), SharedConstants::MONO_ANDROID_ASSEMBLY_NAME.data ()); - Mono_Android_image = mono_assembly_get_image (Mono_Android_dll); - AndroidEnvironment = mono_class_from_name (Mono_Android_image, SharedConstants::ANDROID_RUNTIME_NS_NAME.data (), SharedConstants::ANDROID_ENVIRONMENT_CLASS_NAME.data ()); - AndroidEnvironment_NotifyTimeZoneChanged = mono_class_get_method_from_name (AndroidEnvironment, "NotifyTimeZoneChanged", 0); - - if (AndroidEnvironment_NotifyTimeZoneChanged == nullptr) { - Helpers::abort_application ("Unable to find Android.Runtime.AndroidEnvironment.NotifyTimeZoneChanged()!"); - } -} - -static void -clear_time_zone_caches_within_domain ([[maybe_unused]] void *user_data) -{ - mono_runtime_invoke ( - AndroidEnvironment_NotifyTimeZoneChanged, /* method */ - nullptr, /* obj */ - nullptr, /* args */ - nullptr /* exc */ - ); -} - -static void -clear_time_zone_caches (MonoDomain *domain, void *user_data) -{ - mono_thread_create (domain, reinterpret_cast (clear_time_zone_caches_within_domain), user_data); -} - -extern "C" JNIEXPORT void -JNICALL Java_mono_android_Runtime_notifyTimeZoneChanged ([[maybe_unused]] JNIEnv *env, [[maybe_unused]] jclass klass) -{ - init (); - mono_domain_foreach (clear_time_zone_caches, nullptr); -} diff --git a/tests/MSBuildDeviceIntegration/Resources/LinkDescTest/PreserveTest.cs b/tests/MSBuildDeviceIntegration/Resources/LinkDescTest/PreserveTest.cs index d822000a399..84c99b6c145 100644 --- a/tests/MSBuildDeviceIntegration/Resources/LinkDescTest/PreserveTest.cs +++ b/tests/MSBuildDeviceIntegration/Resources/LinkDescTest/PreserveTest.cs @@ -7,13 +7,12 @@ public class PreserveTest public static string MethodsArePreserved () { try { - // See src/monodroid/jni/timezones.cc for usage - var androidEnvironment = Type.GetType ("Android.Runtime.AndroidEnvironment, Mono.Android", throwOnError: true); - var notifyTimeZoneChanged = androidEnvironment.GetMethod ("NotifyTimeZoneChanged", BindingFlags.Static | BindingFlags.NonPublic); - if (notifyTimeZoneChanged == null) { - return $"[FAIL] {nameof (PreserveTest)}.{nameof (MethodsArePreserved)} FAILED: {nameof (notifyTimeZoneChanged)} is null)"; + // See src/Microsoft.Android.Sdk.ILLink/PreserveLists/Mono.Android.xml + var javaLangObject = Type.GetType ("Java.Lang.Object, Mono.Android", throwOnError: true); + var setHandleOnDeserialized = javaLangObject.GetMethod ("SetHandleOnDeserialized", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + if (setHandleOnDeserialized == null) { + return $"[FAIL] {nameof (PreserveTest)}.{nameof (MethodsArePreserved)} FAILED: {nameof (setHandleOnDeserialized)} is null)"; } - notifyTimeZoneChanged.Invoke (null, null); return $"[PASS] {nameof (PreserveTest)}.{nameof (MethodsArePreserved)}"; } catch (Exception ex) { return $"[FAIL] {nameof (PreserveTest)}.{nameof (MethodsArePreserved)} FAILED: {ex}"; From 585c39dfed58f55be504186230f36305479d8e16 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Mon, 27 Jul 2026 12:23:11 -0500 Subject: [PATCH 2/2] Remove PreserveTest device test case Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6e170150-3891-4cdc-afa9-b8f9d62f1cfd --- .../LinkDescTest/MainActivityReplacement.cs | 3 --- .../Resources/LinkDescTest/PreserveTest.cs | 21 ------------------- .../Tests/InstallAndRunTests.cs | 3 --- 3 files changed, 27 deletions(-) delete mode 100644 tests/MSBuildDeviceIntegration/Resources/LinkDescTest/PreserveTest.cs diff --git a/tests/MSBuildDeviceIntegration/Resources/LinkDescTest/MainActivityReplacement.cs b/tests/MSBuildDeviceIntegration/Resources/LinkDescTest/MainActivityReplacement.cs index 6136877cbc5..6006a938a42 100644 --- a/tests/MSBuildDeviceIntegration/Resources/LinkDescTest/MainActivityReplacement.cs +++ b/tests/MSBuildDeviceIntegration/Resources/LinkDescTest/MainActivityReplacement.cs @@ -99,9 +99,6 @@ protected override void OnCreate(Bundle bundle) // [Test] Post Android.Util.Log.Info(TAG, HttpClientTest.Post ()); - // [Test] MethodsArePreserved - Android.Util.Log.Info (TAG, PreserveTest.MethodsArePreserved ()); - // [Test] TextChanged Android.Util.Log.Info (TAG, MaterialTextChanged.TextChanged (this)); diff --git a/tests/MSBuildDeviceIntegration/Resources/LinkDescTest/PreserveTest.cs b/tests/MSBuildDeviceIntegration/Resources/LinkDescTest/PreserveTest.cs deleted file mode 100644 index 84c99b6c145..00000000000 --- a/tests/MSBuildDeviceIntegration/Resources/LinkDescTest/PreserveTest.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Reflection; - -public class PreserveTest -{ - // [Test] - public static string MethodsArePreserved () - { - try { - // See src/Microsoft.Android.Sdk.ILLink/PreserveLists/Mono.Android.xml - var javaLangObject = Type.GetType ("Java.Lang.Object, Mono.Android", throwOnError: true); - var setHandleOnDeserialized = javaLangObject.GetMethod ("SetHandleOnDeserialized", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); - if (setHandleOnDeserialized == null) { - return $"[FAIL] {nameof (PreserveTest)}.{nameof (MethodsArePreserved)} FAILED: {nameof (setHandleOnDeserialized)} is null)"; - } - return $"[PASS] {nameof (PreserveTest)}.{nameof (MethodsArePreserved)}"; - } catch (Exception ex) { - return $"[FAIL] {nameof (PreserveTest)}.{nameof (MethodsArePreserved)} FAILED: {ex}"; - } - } -} diff --git a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs index f854e6f70c6..29838971271 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs @@ -1230,9 +1230,6 @@ public class LinkModeFullClass { new BuildItem.Source ("LocalTestServers.cs") { TextContent = () => File.ReadAllText (Path.Combine (XABuildPaths.TopDirectory, "tests", "Mono.Android-Tests", "Mono.Android-Tests", "Xamarin.Android.Net", "LocalTestServers.cs")) }, - new BuildItem.Source ("PreserveTest.cs") { - TextContent = () => getResource("PreserveTest") - }, }, }; lib2.SetRuntime (runtime);