Skip to content
Open
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
12 changes: 12 additions & 0 deletions build-tools/automation/azure-pipelines-nightly.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,18 @@ stages:

- template: /build-tools/automation/yaml-templates/fail-on-issue.yaml

- template: /build-tools/automation/yaml-templates/run-emulator-tests.yaml
parameters:
jobName: TimeZoneChangeTests
jobTimeout: 120
use1ESTemplate: false
testSteps:
- template: run-nunit-tests.yaml
parameters:
testRunTitle: TimeZoneChangeTests On Device - macOS
testAssembly: $(System.DefaultWorkingDirectory)/bin/Test$(XA.Build.Configuration)/MSBuildDeviceIntegration/$(DotNetStableTargetFramework)/MSBuildDeviceIntegration.dll
dotNetTestExtraArgs: --filter "Name=TimeZoneChangeClearsCachedTimeZoneInfo"


# Localization test jobs
- stage: test_locals
Expand Down
2 changes: 1 addition & 1 deletion src/Mono.Android/Android.Runtime/AndroidEnvironment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ static Java.Security.Cert.X509Certificate ConvertCertificate (Java.Security.Cert

// This is invoked by libmonodroid.so.
// DO NOT REMOVE
static void NotifyTimeZoneChanged ()
internal static void NotifyTimeZoneChanged ()
{
var thread = Thread.CurrentThread;
var timeZoneClearInfo = new[]{
Expand Down
12 changes: 12 additions & 0 deletions src/Mono.Android/Android.Runtime/JNIEnvInit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ internal struct JnienvInitializeArgs {
public IntPtr grefGCUserPeerable;
public IntPtr propagateUncaughtExceptionFn;
public IntPtr registerJniNativesFn;
public IntPtr notifyTimeZoneChangedFn;
}
#pragma warning restore 0649

Expand All @@ -56,6 +57,16 @@ static void PropagateUncaughtException (IntPtr env, IntPtr javaThread, IntPtr ja
JNIEnv.PropagateUncaughtException (env, javaThread, javaException);
}

[UnmanagedCallersOnly]
static void NotifyTimeZoneChanged ()
{
try {
AndroidEnvironment.NotifyTimeZoneChanged ();
} catch (Exception e) {
Logger.Log (LogLevel.Error, "MonoAndroid", $"Exception while clearing time-zone caches: {e}");
}
}

[UnmanagedCallersOnly]
[RequiresUnreferencedCode ("Uses reflection to access System.StartupHookProvider.")]
static unsafe void RegisterJniNatives (IntPtr typeName_ptr, int typeName_len, IntPtr jniClass, IntPtr methods_ptr, int methods_len)
Expand Down Expand Up @@ -145,6 +156,7 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args)
RegisterTrimmableTypeMapNativeMethodsIfNeeded ();

args->propagateUncaughtExceptionFn = (IntPtr)(delegate* unmanaged<IntPtr, IntPtr, IntPtr, void>)&PropagateUncaughtException;
args->notifyTimeZoneChangedFn = (IntPtr)(delegate* unmanaged<void>)&NotifyTimeZoneChanged;

if (!RuntimeFeature.TrimmableTypeMap) {
args->registerJniNativesFn = GetRegisterJniNativesFnPtr ();
Expand Down
2 changes: 1 addition & 1 deletion src/native/clr/host/host-jni.cc
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,5 @@ JNICALL Java_mono_android_Runtime_propagateUncaughtException (JNIEnv *env, [[may
JNIEXPORT void
JNICALL Java_mono_android_Runtime_notifyTimeZoneChanged ([[maybe_unused]] JNIEnv *env, [[maybe_unused]] jclass klass)
{
// TODO: implement or remove
Host::notify_time_zone_changed ();
}
12 changes: 12 additions & 0 deletions src/native/clr/host/host.cc
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,9 @@ void Host::Java_mono_android_Runtime_initInternal (
// trimmable typemap path (the method is trimmed; registration is handled in managed code).
jnienv_register_jni_natives = init.registerJniNativesFn;
jnienv_propagate_uncaught_exception = init.propagateUncaughtExceptionFn;
jnienv_notify_time_zone_changed = init.notifyTimeZoneChangedFn;
abort_unless (jnienv_propagate_uncaught_exception != nullptr, "Failed to obtain unmanaged-callers-only function pointer to the PropagateUncaughtException method.");
abort_unless (jnienv_notify_time_zone_changed != nullptr, "Failed to obtain unmanaged-callers-only function pointer to the NotifyTimeZoneChanged method.");

if (FastTiming::enabled ()) [[unlikely]] {
internal_timing.end_event (); // native to managed
Expand Down Expand Up @@ -554,3 +556,13 @@ void Host::propagate_uncaught_exception (JNIEnv *env, jobject javaThread, jthrow

jnienv_propagate_uncaught_exception (env, javaThread, javaException);
}

void Host::notify_time_zone_changed () noexcept
{
if (jnienv_notify_time_zone_changed == nullptr) {
log_warnf (LOG_DEFAULT, "notify_time_zone_changed called before JNIEnvInit.NotifyTimeZoneChanged was initialized");
return;
}
Comment thread
jonathanpeppers marked this conversation as resolved.

jnienv_notify_time_zone_changed ();
}
2 changes: 2 additions & 0 deletions src/native/clr/include/host/host.hh
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ namespace xamarin::android {
static void Java_mono_android_Runtime_register (JNIEnv *env, jstring managedType, jclass nativeClass, jstring methods) noexcept;
static void Java_mono_android_Runtime_registerNatives (JNIEnv *env, jclass nativeClass) noexcept;
static void propagate_uncaught_exception (JNIEnv *env, jobject javaThread, jthrowable javaException) noexcept;
static void notify_time_zone_changed () noexcept;

static auto get_timing () -> std::shared_ptr<Timing>
{
Expand Down Expand Up @@ -58,6 +59,7 @@ namespace xamarin::android {
static inline bool found_assembly_store = false;
static inline jnienv_register_jni_natives_fn jnienv_register_jni_natives = nullptr;
static inline jnienv_propagate_uncaught_exception_fn jnienv_propagate_uncaught_exception = nullptr;
static inline jnienv_notify_time_zone_changed_fn jnienv_notify_time_zone_changed = nullptr;

static inline jclass java_TimeZone = nullptr;

Expand Down
2 changes: 2 additions & 0 deletions src/native/common/include/managed-interface.hh
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ namespace xamarin::android {

using jnienv_propagate_uncaught_exception_fn = void (*)(JNIEnv *env, jobject javaThread, jthrowable javaException);
using jnienv_register_jni_natives_fn = void (*)(const jchar *typeName_ptr, int32_t typeName_len, jclass jniClass, const jchar *methods_ptr, int32_t methods_len);
using jnienv_notify_time_zone_changed_fn = void (*)();

// NOTE: Keep this in sync with managed side in src/Mono.Android/Android.Runtime/JNIEnvInit.cs
struct JnienvInitializeArgs {
Expand All @@ -37,6 +38,7 @@ namespace xamarin::android {
jobject grefGCUserPeerable;
jnienv_propagate_uncaught_exception_fn propagateUncaughtExceptionFn;
jnienv_register_jni_natives_fn registerJniNativesFn;
jnienv_notify_time_zone_changed_fn notifyTimeZoneChangedFn;
};

// Keep the enum values in sync with those in src/Mono.Android/AndroidRuntime/BoundExceptionType.cs
Expand Down
112 changes: 112 additions & 0 deletions tests/MSBuildDeviceIntegration/Tests/TimeZoneInfoTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -259,4 +259,116 @@ public void CheckTimeZoneInfoIsCorrect (string timeZone)
Assert.IsTrue (logLine.Contains (expectedLogcatOutput), $"Line '{logLine}' did not contain '{expectedLogcatOutput}'");
}
}

[TestFixture (AndroidRuntime.CoreCLR)]
[Category ("TimeZoneInfo")]
[NonParallelizable]
public class TimeZoneChangeTests : DeviceTest
{
const string InitialTimeZone = "America/New_York";
const string ChangedTimeZone = "America/Los_Angeles";

readonly AndroidRuntime runtime;
ProjectBuilder builder;
XamarinAndroidApplicationProject proj;
string originalAutoTimeZone;
string originalTimeZone;

public TimeZoneChangeTests (AndroidRuntime runtime)
{
this.runtime = runtime;
}

[OneTimeSetUp]
public void BeforeAllTests ()
{
AssertHasDevices ();

originalAutoTimeZone = RunAdbCommand ("shell settings get global auto_time_zone").Trim ();
originalTimeZone = RunAdbCommand ("shell getprop persist.sys.timezone").Trim ();
RunAdbCommand ("shell settings put global auto_time_zone 0");
SetTimeZone (InitialTimeZone);

proj = new XamarinAndroidApplicationProject (packageName: PackageUtils.MakePackageName (runtime, "TimeZoneChangeTests"));
proj.SetRuntime (runtime);
proj.MainActivity = proj.DefaultMainActivity.Replace ("//${AFTER_ONCREATE}", """
if (button == null) {
throw new InvalidOperationException ("Could not find the test button.");
}

Console.WriteLine ($"TimeZoneChangeTests.Initial={TimeZoneInfo.Local.Id}");
button.Click += delegate {
Console.WriteLine ($"TimeZoneChangeTests.Changed={TimeZoneInfo.Local.Id}");
};
""");

builder = CreateApkBuilder (Path.Combine ("temp", TestName));
builder.BuildLogFile = "timezone-change-install.log";
Assert.IsTrue (builder.Install (proj), "Install should have succeeded.");
}

[OneTimeTearDown]
protected override void AfterAllTests ()
{
try {
if (proj != null) {
RunAdbCommand ($"shell am force-stop --user all {proj.PackageName}");
}
if (!string.IsNullOrEmpty (originalTimeZone)) {
SetTimeZone (originalTimeZone);
}
if (!string.IsNullOrEmpty (originalAutoTimeZone)) {
RunAdbCommand ($"shell settings put global auto_time_zone {originalAutoTimeZone}");
}
} finally {
base.AfterAllTests ();
}
}
Comment thread
jonathanpeppers marked this conversation as resolved.

[Test]
public void TimeZoneChangeClearsCachedTimeZoneInfo ()
{
ClearAdbLogcat ();
StartActivityAndAssert (proj);

string initialLog = $"TimeZoneChangeTests.Initial={InitialTimeZone}";
Assert.IsTrue (
MonitorAdbLogcat (
line => line.Contains (initialLog),
Path.Combine (Root, builder.ProjectDirectory, "timezone-change-initial.log"),
45
),
$"App output did not contain '{initialLog}'"
);

ClearAdbLogcat ();
SetTimeZone (ChangedTimeZone);
Thread.Sleep (1000);
Assert.IsTrue (ClickButton (proj.PackageName, "myButton", "HELLO WORLD, CLICK ME!"), "Test button should have been clicked.");

string changedLog = $"TimeZoneChangeTests.Changed={ChangedTimeZone}";
Assert.IsTrue (
MonitorAdbLogcat (
line => line.Contains (changedLog),
Path.Combine (Root, builder.ProjectDirectory, "timezone-change-updated.log"),
45
),
$"App output did not contain '{changedLog}'"
);
}

static void SetTimeZone (string timeZone)
{
string deviceTimeZone = "";
for (int attempt = 0; attempt < 5; attempt++) {
RunAdbCommand ($"shell cmd alarm set-timezone \"{timeZone}\"");
deviceTimeZone = RunAdbCommand ("shell getprop persist.sys.timezone").Trim ();
if (deviceTimeZone == timeZone) {
break;
}
}

Assert.AreEqual (timeZone, deviceTimeZone, $"The command to set the device timezone to {timeZone} failed. Current device timezone is {deviceTimeZone}");
}
}
}
Loading