Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resuming app from background service results in System.InvalidOperationException MauiContext is null. #25443

Open
RobbiewOnline opened this issue Oct 22, 2024 · 7 comments
Assignees
Labels
migration-compatibility Xamarin.Forms to .NET MAUI Migration, Upgrade Assistant, Try-Convert platform/android 🤖 s/needs-attention Issue has more information and needs another look t/bug Something isn't working

Comments

@RobbiewOnline
Copy link

RobbiewOnline commented Oct 22, 2024

Description

Detected System.InvalidOperationException MauiContext is null via sentry.io when the app was resumed by the user approx 10 minutes of background services.

It's assumed the user either resumed the app, or clicked not the foreground service notification to resume the app.

The call stack is all deep within MAUI so I'm not sure how to provide more details that will help diagnose this?

Image

Steps to Reproduce

Published my app to Google Play for internal testing then started seeing these exceptions, it's unclear what the user was doing, but logs indicates the following:

  • User launches app
  • The app captures GPS updates using an Android background service
  • About 10 minutes elapses since the users last UI interaction, the app is assumed to be in the background during this time
  • The user resumes the app which displays our PleaseWaitPage page
  • The app is automatically navigated to a dashboard page which includes a tabbed page with three tabs
  • The 'System.InvalidOperationException: MauiContext is null' is then thrown causing an unhandled exception, it's unclear whether this is the dashboard page or tabbed page as the call stack is all MAUI centric / low-level.

Note that Sentry shows this is .NET 9.0.0-rc.2.24473.5 whereas the drop-down in this ticket only let's me choose 9.0.0-rc.2.24503.2 as the closest match.

The Android activity is set to be LaunchMode = LaunchMode.SingleTask

Link to public reproduction project repository

No response

Version with bug

9.0.0-rc.2.24503.2

Is this a regression from previous behavior?

Yes, this used to work in Xamarin.Forms

Last version that worked well

Unknown/Other

Affected platforms

Android

Affected platform versions

Android 13 / CMA-LX1 + Android 14 / Pixel 8

Did you find any workaround?

Yes - within the CreateWindow method I now return the existing Window if one has previously been created, e.g.

        Window originalWindow = null;

	protected override Window CreateWindow(IActivationState activationState)
        {

            if (originalWindow != null) {
                return originalWindow;
            }

            originalWindow = new Window(PageManager.GetPleaseWaitPage());
            return originalWindow;
        }

Relevant log output

No response

@RobbiewOnline RobbiewOnline added the t/bug Something isn't working label Oct 22, 2024
@PureWeen PureWeen modified the milestones: .NET 9 SR1, .NET 9.0 GA Oct 22, 2024
@PureWeen PureWeen self-assigned this Oct 22, 2024
@PureWeen
Copy link
Member

@RobbiewOnline can you post any code with how you handle Window and setting the mainpage on your application?

Are you reusing the same window across activity recreates?
Reusing the same page?

@PureWeen
Copy link
Member

Nm, I repro'd

@PureWeen PureWeen added the migration-compatibility Xamarin.Forms to .NET MAUI Migration, Upgrade Assistant, Try-Convert label Oct 22, 2024
@PureWeen
Copy link
Member

PureWeen commented Oct 22, 2024

Actually @RobbiewOnline

Can you post the code of how you setup the main page you are setting on the app/window?

I'm curious to see your scenario.
This can happen if you reuse the same page on different window instances.

Possible fix here
https://github.com/dotnet/maui/tree/fix_25443

@PureWeen PureWeen added the s/needs-info Issue needs more info from the author label Oct 22, 2024
@RobbiewOnline
Copy link
Author

Hi,

I wasn't doing anything 'special' in my code, I was following the standard code sample that does the following:

        protected override Window CreateWindow(IActivationState activationState)
        {
                 return new Window(PageManager.GetPleaseWaitPage()); 
        }

But it would seem when the application is being returned to from a background service, which uses an intent to launch / return to the app, the CreateWindow is called again, which promptly creates a new Window.

Somewhere in this lifecycle I get behavioural issues, like the error previously reported.

I modified the code this evening to keep a reference to the original Window created and if a reference remains then re-use it.

This now launches from the persistent foreground service notification 🕺

So app.xaml.cs window registration becomes this...

        Window originalWindow = null;

	protected override Window CreateWindow(IActivationState activationState)
        {

            if (originalWindow != null) {
                Logger.LogWithoutStorage(this, $"App CreateWindow invoked - reusing originalWindow");
                return originalWindow;
            }

            originalWindow = new Window(PageManager.GetPleaseWaitPage()); // Note can't call base.CreateWindow(activationState);
            return originalWindow;
        }

I don't know if this is a hack/work-around, or what I should have been doing in the first place?

The MainActivity is defined like this, most of it's logging because I was trying to track down the crashes / ANRs.

    [Activity(
        // Label = "MyTeamSafe"
        // Icon = "@mipmap/icon"
        //Theme = "@style/MainTheme",
        Theme = "@style/Maui.SplashTheme",

        MainLauncher = true,
        Exported = true,
        LaunchMode = LaunchMode.SingleTask, // Required for Media playback
        ScreenOrientation = ScreenOrientation.Portrait,
        ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
    public class MainActivity : MauiAppCompatActivity
    {
        public MainActivity()
        {
            Logger.LogWithoutStorage(this, $"MainActivity() Android SDK: {Build.VERSION.SdkInt}, Codename: {Build.VERSION.Codename} stacktrace:{new StackTrace().ToString()}");   
        }

        protected override void OnCreate(Bundle bundle)
        {
            try
            {

                Logger.LogWithoutStorage(this, $"OnCreate invoked stacktrace:{new StackTrace().ToString()}");   

                Logger.LogWithoutStorage(this, $"OnCreate IgnoreDeleteNotifications=false");
                PlatformDifferences.IgnoreDeleteNotifications = false; // Only clears if launching interactively (not a notification)

                base.OnCreate(bundle);

                Platform.Init(this, bundle);

                CrossFingerprint.SetCurrentActivityResolver(() => Platform.CurrentActivity);

                Logger.LogWithoutStorage(this, "OnCreate passing intent to Shove");

                try
                {
                    ((ShovePlugin)CrossShovePlugin.Current)?.OnNewIntent(Platform.CurrentActivity, Intent);
                }
                catch (Exception e)
                {
                    CrashHelper.HandleException(false, "Couldn't delegate intent to Shove", this, e);
                }
            }
            catch (Exception ex)
            {
                Logger.LogWithoutStorage(this, $"OnCreate exception caught reason: {ex.Message}");
            }

            Logger.LogWithoutStorage(this, "OnCreate - done");

        }

For context the persistent notification is setup like this (it's a requirement of Android if you want to promote a GPS service to the foreground) and clicking on the notification (after swiping away the app) would result in a new window being created even though the app hasn't been terminated (because it's running as a foreground service), at least that's my interpretation of it ...

        private void registerForPersistentNotificationService()
        {
            if (!NeedsToRegisterForPersitentNotification)
            {
                Logger.Log(this, $"registerForService started NeedsToRegisterForPersitentNotification:" + NeedsToRegisterForPersitentNotification + " - ignored (already done)");
            }
            else
            {
                Logger.Log(this, $"registerForService started NeedsToRegisterForPersitentNotification:" + NeedsToRegisterForPersitentNotification);

                NeedsToRegisterForPersitentNotification = false;

                if (Build.VERSION.SdkInt >= BuildVersionCodes.O) // Oreo = Android SDK 26 / Android 8.0+
                {
#pragma warning disable CA1416 // Validate platform compatibility
                    Logger.Log(this, $"registerForService Oreo+ is creating channel");

                    var CHANNEL_ID = "MyApp-BackgroundService";
                    var channel = new NotificationChannel(CHANNEL_ID, "Channel", NotificationImportance.Default)
                    {
                        Description = "Foreground Service Channel"
                    };

                    Logger.Log(this, $"registerForService creating notification channel");

                    var notificationManager = (NotificationManager)GetSystemService(NotificationService);
                    notificationManager.CreateNotificationChannel(channel);

                    Logger.Log(this, $"registerForService creating notification");

                    var nb = new Notification.Builder(this, CHANNEL_ID);
                    setCommonNotification(nb);

                    var notification = nb.Build();

                    // Enlist this instance of the service as a foreground service
                    Logger.Log(this, $"registerForService starting foreground service");
                    StartForeground(SERVICE_RUNNING_NOTIFICATION_ID, notification);
#pragma warning restore CA1416 // Validate platform compatibility
                }
                else if (Build.VERSION.SdkInt >= BuildVersionCodes.Lollipop) // Lollipop = Android SDK 21 / Android 5.0+
                {
                    // Lollipop and onwards
                    Logger.Log(this, $"registerForService Lollipop+ - creating notification");

                    var nb = new Notification.Builder(this);
                    setCommonNotification(nb);

                    var notification = nb
                        .Build();

                    // Enlist this instance of the service as a foreground service
                    Logger.Log(this, $"registerForService starting service");
                    StartForeground(SERVICE_RUNNING_NOTIFICATION_ID, notification);
                } else {
                    Logger.Log(this, $"registerForService Pre-Lollipop - not supported");
                }
            }

            Logger.Log(this, $"registerForService completed");

        }

        private void setCommonNotification(Notification.Builder nb)
        {

            var activity = new Intent(MauiApplication.Current.ApplicationContext, typeof(MainActivity));

            PendingIntent pendingIntent = null;
            if (Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.S)
            {
                pendingIntent = PendingIntent.GetActivity(this, 0, activity, PendingIntentFlags.Immutable);
            }
            else
            {
                pendingIntent = PendingIntent.GetActivity(this, 0, activity, 0);
            }

            nb.SetContentTitle("MyApp");
            nb.SetContentText(Text);
            nb.SetOngoing(true);
            nb.SetContentIntent(pendingIntent);

            if (Android.OS.Build.VERSION.SdkInt < Android.OS.BuildVersionCodes.O)
            {
                nb.SetSmallIcon(Resource.Drawable.icon_as_resource);
            } else
            {
                nb.SetSmallIcon(Resource.Drawable.ic_notification_icon_as_resource);
                nb.SetColor(Resource.Color.colorAccent); // Resource.Drawable.icon
            }

        }

Your thoughts are very much appreciated as I'd like to understand whether this is a MAUI lifecycle bug (resuming an app from a persistent foreground notification), or my missunderstanding of creating a Window and assuming when CreateWindow is called it should be created each invocation.

@dotnet-policy-service dotnet-policy-service bot added s/needs-attention Issue has more information and needs another look and removed s/needs-info Issue needs more info from the author labels Oct 22, 2024
@PureWeen
Copy link
Member

Yea, I think this is an area where we could throw a better exception or make the code run in a little bit more helpful of a fashion.
Like, perhaps we should just auto remove a page from a previous window...

I've moved this into our servicing milestone so we can give it a little more thought about best bath here

I think your workaround is "valid"

My suggestion was going to either be

  1. what you did
  2. on the 'originalWindow' you could set the Page to null and I think that'll work as well

so like this might even work

var waitPage = PageManager.GetPleaseWaitPage();
if (waitPage.Window is not null)
     waitPage.Window.Page = null;

return new Window(waitPage); 

@RobbiewOnline
Copy link
Author

Thanks @PureWeen

From my perspsective if the original Page(s) and Window objects are all still referencable, then there's no point creating a new Window and trying to reassociate the pages to the new Window?

It feels like in some Android specific lifecycles (e.g. resuming the app from a background/foreground service system tray banner) then the CreateWindow call should be avoided as everything is still working as-is, no new Window needs to be created.

I'm going to tag this issue in with #25274 as these might be related.

@PureWeen
Copy link
Member

PureWeen commented Oct 24, 2024

Thanks @PureWeen

From my perspsective if the original Page(s) and Window objects are all still referencable, then there's no point creating a new Window and trying to reassociate the pages to the new Window?

It feels like in some Android specific lifecycles (e.g. resuming the app from a background/foreground service system tray banner) then the CreateWindow call should be avoided as everything is still working as-is, no new Window needs to be created.

I'm going to tag this issue in with #25274 as these might be related.

Possibly, the problem is, is that we have limited context as to why CreateWindow is being called. These are all just tied to platform life cycle events.

Maybe the user wants a new activity to handle a new intent? maybe they want to handle notifications in a new intent/window?

We could add some type registration hooks, or a way for users to specify that an app is a single page app and to always reuse the window/page as singletons.

I don't really want to make assumptions for the user why CreateWindow is being called but we could probably add some ways for users to express that intent so it's not as leaky.

For example, in your scenario you are already using a singleton Page so why aren't you extending that concept to your window?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
migration-compatibility Xamarin.Forms to .NET MAUI Migration, Upgrade Assistant, Try-Convert platform/android 🤖 s/needs-attention Issue has more information and needs another look t/bug Something isn't working
Projects
Status: Todo
Development

No branches or pull requests

2 participants