androidmaui

How to get Maui Android SingleTop, SingleTask working as documented


I am writing a Maui Android app which receives files shared from WhatsApp or Telegram.

There is only one activity defined and it is created as SingleTask.

However whenever I resume the app via the task switcher, OnCreate is called instead of OnNewIntent. This in itself is not a problem, but when I share a file from say WhatsApp, an entire new instance of the app is created, so that I have two instances of the app running at the same time. This is causing crashes.

If I change the launchMode to SingleTop or SingleInstance, I get exactly the same unwanted behaviour.

Here is my code. What is going on?

EDIT 1 I've been researching this more and one further thing that needs to be mentioned is that my app is being created as a child of the WhatsApp process. When I look at the task switcher, I have one task which is MyApp and one task that is WhatsApp but the WhatsApp's UI is another instance of MyApp which has been overlayed over the top of the normal WhatsApp UI.

Even more strange is both 'instances' of MyApp seem to be sharing the same activity instance values and window. So it is the same instance of the activity visually split into two in the Android system.

EDIT 2 Done more investigation, now when switching back and forth to the app I'm getting the follow error:

System.InvalidOperationException: 'This window is already associated with an active Activity (MyApp.MainActivity). Please override CreateWindow on MyApp.App to add support for multiple activities https://aka.ms/maui-docs-create-window or set the LaunchMode to SingleTop on MyApp.MainActivity.'

Again, changing the manifest and MainActivity to SingleTop has no effect whatsoever.

EDIT 3 Done more investigation, and discovered that setting NoHistory = false helps the activity not get destroyed between each task switch. This helps solve some of my problem. But my app is still being recreated as a child of WhatsApp or Telegram when sharing from there. Still need this problem solved.

EDIT 4 I've run into a new, related problem.

In my new LaunchActivity (as suggested by Steve below, also see updated code below), when OnCreate and base.OnCreate are called during a share operation, a new AppShell is created with its own associated flyout menu.

The problem is the new AppShell is 'broken', i.e. it doesn't respond to FlyoutIsPresented = false statements, and a ListView on the flyout page doesn't display any content, despite the ItemSource indicating it contains several hundred records.

Is there anyway for the app to be relaunched without the breaking, or even the recreation, of these elements?

(Note this 'broken' shell behaviour doesn't occur when the app is launched from the launcher. Only when it is already running in the background and re-launched from a WhatsApp or Telegram share file operation).

Code

    [Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, 
            LaunchMode = LaunchMode.SingleTask, 
            NoHistory = true,
            Exported = true,
            TaskAffinity = "com.mycompany.myapp",
            ConfigurationChanges =  ConfigChanges.ScreenSize | ConfigChanges.Orientation | 
                                    ConfigChanges.UiMode | ConfigChanges.ScreenLayout | 
                                    ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
    
        //whatsapp & telegram send binaries as octet-stream
        [IntentFilter(new[] { Android.Content.Intent.ActionSend },
                    Categories = new[] { Android.Content.Intent.CategoryDefault },
                    DataMimeType = @"application/octet-stream", 
                    Icon = "@mipmap/appicon")]
    
    
    public partial class LaunchActivity : MauiAppCompatActivity
    {
        protected override void OnCreate(Bundle bundle)
        {
            try
            {
                base.OnCreate(bundle);
                Platform.Init(this, bundle);


                if (Intent.Action == Intent.ActionSend || Intent.Action == Intent.ActionView)
                    ProcessSendOrViewIntent();

                LaunchMainActivity();

                Finish();
            }
            catch (Exception e)
            {
                LogException(e, false);
                throw;
            }
        }

        void LaunchMainActivity()
        {
            Intent intent = new Intent(this, typeof(MainActivity));
            intent.AddFlags(ActivityFlags.ClearTop | ActivityFlags.SingleTop);
            StartActivity(intent);
        }
    }

Solution

  • It's a subtlety of Android tasks.

    [Below code is not tested. Might be incomplete.]

    IMPORTANT: DO NOT implement the suggestion add support for multiple activities".
    As you might suspect, that is going in the opposite direction from what you want.


    CAUSE:

    The problem occurs when your app's activity is created as a child of a different app; your activity is created within the other app's task:

    For example, if an email message ["WhatsApp"] contains a link to a web page ["your activity"], clicking the link brings up an activity that can display the page. That activity is defined by the browser application ["your app"] but is launched as part of the email task ["WhatsApp"].

    CONSEQUENCE: Carefully read the definitions of SingleTop and SingleInstance in Android LaunchMode.

    Note what they say about target task. LaunchMode defines behavior within a task. Because WhatsApp is a different task, when your activity is created in it, your existing instance in your task is not relevant! It is independent. Ignored.


    FIX:

    The next sentence in the link above, after the quote above, is:

    If it's reparented to the browser task, it shows when the browser ["your app"] next comes to the front and is absent when the email task again comes forward.

    [Activity(LaunchActivity = true, ..., AllowTaskReparenting = true)]
    public class MainActivity : Activity
    

    Verify this works:

    That is, WhatsApp should show whatever page you were on when you clicked something that launched your app.
    And if you toggle between your app and WhatsApp (device's home screen or app switcher), they should each show their own contents.


    Question: Did it automatically switch to your app? If so, and your app and WhatsApp no longer are entangled, then you are done.



    A. Create a new Activity, SwitchboardActivity. (Or perhaps LaunchActivity.)
    Move some of the attributes on your MainActivity to SwitchboardActivity:

    [Activity(LaunchActivity = true, AllowTaskReparenting = true)]
    public class SwitchboardActivity : Activity
    

    Other attributes either stay on MainActivity or move also or are needed both places. As appropriate.

    B. In OnCreate (or maybe OnResume), SwitchboardActivity needs to:

    public class MainActivity : Activity
    {
        public static It { get; set; ]
    
        public MainActivity()
        {
            It = this;
        }
    }
    
    [Activity(LaunchActivity = true, AllowTaskReparenting = true)]
    public class SwitchboardActivity : Activity
    {
        ... override OnCreate(...)
        {
            base.OnCreate(...);
            var main = MainActivity.It;
            bool exists = (main != null);
            if (!exists)
                main = new MainActivity();
            // TODO: `Intent` code to switch to "main". Details may be different based on `if (exists)`.
            // TODO: If create new MainActivity, how make it root of new task? How make sure `back` button does not go back to SwitchboardActivity?
    }
    

    [Exact code TBD. If you get "Step 3" working, add "Your Answer" with that code.]