0

Our app opens up with ActMain activity; in order to proceed users must authenticate with "Controller" (Windows service running elsewhere). On success ActList activity is loaded and a CommSvc foreground service is started, which together communicate with Controller and visualize incoming messages. Every received message is added to the top of the list in ActList. If ActList is not the topmost activity (because another app has been opened), CommSvc will add a regular auto-cancelling notification (with vibra- and audio-alert) about new incoming message. Tapping these notifications activates ActList.

Service notification provides 3 actions (as shown in the screenshot below):
Tap to open. - activates ActList (brings it to the top)
Log Out - closes ActList and stops the service (the user is logged-out upon resuming ActMain activity)
Exit - does the same as Log Out and in addition closes the app

1

Closing ActList normally (using device's Back button or LogOut | Exit actions) removes all notifications in override of Service.onDestroy( ) (Android, Xamarin) and has no consequences. No problem. But..

If the app is explicitly removed/closed in the switcher, Service.onDestroy( ) is not called!
Neither is Service.OnTaskRemoved( )!
What??

When the app is dismissed, initially all its notifications disappear. But in a few seconds the service notification returns. If I restart the app quickly the notification is displayed in ActMain - before the code actually starts the FG-service and calls StartForeground( int, Notification ). If the user executes this notification's actions, app context does not match - resulting in a hard crash. If I don't restart the app, the crash message is displayed sometimes.

It's as if OS is trying to preserve the running state of the FG-service, ignoring the fact that the hosting process has been explicitly [and I hope gracefully] dismissed.

4

Even adding the following to App.OnCreate( ) to forcefully clean up all notifications upon startup does not help:

    oNtfMngr =  (NotificationManager) GetSystemService( Context.NotificationService );
    oNtfMngr.CancelAll( );

This is repeatable on Android versions 4.3 (API 18) to 9.0 and probably higher (do not have a 10.0 device at the moment).
The only related question is specific to Nougat only and has no confirmed solution or explanation.

Process Lifecycle uses the word 'kill' with the same semantic as in Windows. Well, killing an app is not expected to give it time to finish and exit gracefully. But at least closing from app switcher should. Don't you think?
UI uses terms like Remove and Close all instead of Kill. To me that implies normal shutdown of a process. The topic of app closing in Android seems to be heretic, but how do you even stop debugging without that? Use Stop command in VisualStudio? Forcefully interrupting normal flow of execution?
How is an app supposed to clean up its resources in case of regular dismissal (not a crash)? Rely on the OS? Well, I did, and .. we're here, cause the OS is doing smth weird and none of the declared APIs work.


Putting philosophical debate aside, how do I prevent / fix this behavior?

Service (non-important code redacted for readability):

StartCommandResult.Sticky or .NotSticky has absolutely no effect whatsoever - neither for the issue, nor for functionality. Which is appropriate?

[Service]
public class    CommSvc : Service
{
    public const string     scActionSvcDuOn     =   "SvcDuOn";
    public const string     scActionSvcLout     =   "SvcLout";
    public const string     scActionSvcExit     =   "SvcExit";
    public const int        icSvcAlertID        =   -1;

    App             app;                        /// global application context

    public override StartCommandResult  OnStartCommand( Intent intent, StartCommandFlags flags, int startId )
    {
        if(  intent != null  )
        {
            if(  string.IsNullOrEmpty( intent.Action )  )
            {
                app =   (App)Application;
                app.oCommSvc =  this;

                using(  Notification.Builder nb =   BuildNotification( "Tap to open.", false )  )
                {
                    using(  Intent  o = new Intent( this, GetType( ) )  )
                    {
                        o.SetAction( CommSvc.scActionSvcDuOn );
                        o.SetAction( CommSvc.scActionSvcLout );
                        AddAction( nb, Android.Resource.Drawable.IcLockIdleLock,
                                        "Log Out",  PendingIntent.GetService( this, 0, o, 0 ) );
                        o.SetAction( CommSvc.scActionSvcExit );
                        AddAction( nb, Android.Resource.Drawable.IcLockPowerOff,
                                        "Exit",     PendingIntent.GetService( this, 0, o, 0 ) );
                    }
                    nb.SetSmallIcon( Resource.Drawable.icoAppSvc );
                    nb.SetOngoing( true );

                    StartForeground( icSvcAlertID, nb.Build( ) );
                }
            }
            else
            if(  intent.Action.Equals( CommSvc.scActionSvcLout )  ||
                 intent.Action.Equals( CommSvc.scActionSvcExit )  )
            {
                if(  app.oActList != null  )        app.oActList.Finish( );

                if(  intent.Action.Equals( CommSvc.scActionSvcExit )  )     app.bAppExit =  true;

                StopSelf( );
            }

    //      return  StartCommandResult.NotSticky;
            return  StartCommandResult.Sticky;
        }
        return  base.OnStartCommand( intent, flags, startId );
    }

    /// <summary>Stops comm thread and the timer</summary>
    public void     StopComm( )
    {
        ..
        app.oNtfMngr.Cancel( icSvcAlertID );
    //  app.oNtfMngr.CancelAll( );
        StopForeground( true );
    //  StopForeground( StopForegroundFlags.Remove );
    }

    public override void    OnDestroy( )
    {
        StopComm( );
        base.OnDestroy( );
    }

    public override void    OnTaskRemoved( Intent rootIntent )
    {
        StopComm( );
        base.OnTaskRemoved( rootIntent );
    }

    public Notification.Builder     BuildNotification( string sText, bool bAlert= true )
    {
        Notification.Builder    nb;

        if(  Build.VERSION.SdkInt < BuildVersionCodes.O  )
            nb =    new Notification.Builder( this );
        else
            nb =    new Notification.Builder( this, NotificationChannel.DefaultChannelId );

        nb.SetSmallIcon( Resource.Drawable.icoAlert )
            .SetContentTitle( App.scTokAppName )
            .SetContentIntent( app.piActList )
            .SetContentText( sText );

        if(  BuildVersionCodes.Lollipop <= Build.VERSION.SdkInt  )
            nb.SetVisibility( NotificationVisibility.Public )
                .SetCategory( Notification.CategoryCall );

        if(  bAlert  )
            if(  Build.VERSION.SdkInt < BuildVersionCodes.O  )
                nb.SetDefaults( NotificationDefaults.Sound | NotificationDefaults.Vibrate );

        nb.SetOngoing( true );
        nb.SetAutoCancel( true );

        return  nb;
    }
}

ActList and service are launched in ActMain upon successful user authentication like so:

using(  Intent  oi =    new Intent( this, typeof( ActList ) )  )
{
    StartActivity( oi );
}
using(  Intent  oi =    new Intent( this, typeof( CommSvc ) )  )
{
    if(  Build.VERSION.SdkInt < BuildVersionCodes.O  )
        StartService( oi );
    else
        StartForegroundService( oi );
}

Normal app exiting (by tapping device's Back button twice) is done by calling this method of Application class:

public void     Exit( )
{
    if(  oSipMngr != null  )        oSipMngr.Dispose( );
    if(  oNtfMngr != null  )        oNtfMngr.CancelAll( );

//  System.Environment.Exit( 0 );
    Java.Lang.JavaSystem.Exit( 0 );
//  Process.KillProcess( Process.MyPid( ) );
}
Astrogator
  • 1,041
  • 11
  • 27
  • Not sure if related/seen: [Xamarin Android I need to kill a Foreground Service and a Notification when app has been closed](https://stackoverflow.com/q/56640641/295004) – Morrison Chang May 28 '21 at 22:29
  • Now that I've seen it, I'm doing the same steps, but neither of breakpoints in `onDestroy` or `onTaskRemoved` is hit upon app dismissal. App will crash on subsequent 1st or 2nd restart. `onDestroy` breakpoint is hit upon stopping the service by `Log Out` or `Exit` actions from service notification - followed by a clean closure. `onTaskRemoved` breakpoint is never hit no matter what I tried. – Astrogator Jun 01 '21 at 21:12

1 Answers1

0

Rather inconveniently organized /presented (and thus subject for a big question of its own), but left as the only place that can help is the View | Other Windows | Device Log in Visual Studio. A lot of digging there narrowed down the points of interest (filtered by PID, extra columns removed, and values shortened):

13:01:40.928 W  ActivityManager Scheduling restart of crashed service com.MYAPP/md5.CommSvc in 508002ms
13:01:40.928 I  ActivityManager Process com.MYAPP (pid 15518) has died.
..
12:57:46.978 I  ActivityManager Crashing app skipping ANR: ProcessRecord{42610008 15518:com.MYAPP/u0a10081} Executing service com.MYAPP/md5.CommSvc
12:57:46.928 W  ActivityManager Timeout executing service: ServiceRecord{428f8780 u0 com.MYAPP/md5.CommSvc}
..
12:55:14.445 I  WindowState     WIN DEATH: Window{424d7e08 u0 com.MYAPP/md5.ActList}
12:55:14.445 W  ActivityManager Scheduling restart of crashed service com.MYAPP/md5.CommSvc in 5000ms
12:55:14.445 I  ActivityManager Process com.MYAPP (pid 15232) has died.
12:55:14.445 I  WindowState     WIN DEATH: Window{42499d20 u0 com.MYAPP/md5.ActMain}

So Android tries to restart my FG-service in 5s after app dismissal, and then (weird!) in ~8.5min.

Today, I noticed this Related question, which led to the solution. Big thanks to SE for linking it!

Originally I didn't define this service in AndroidManifest.xml, relying on Xamarin to do this:

2 3

Neither Xamarin nor Android official documentation declare StopWithTask property!

Once I added a declaration of the service to AndroidManifest.xml with that property set to true:

<service android:stopWithTask="true" android:exported="false" android:name="md5.CommSvc" />

now the service is stopped with app dismissal, and Device Log shows:

15:49:28.639 I  ActivityManager Stopping service com.MYAPP/md5.CommSvc: remove task

I still do not get a breakpoint hit neither in onDestroy() nor in onTaskRemoved() in that case though..
But at least there are no more crashes, or svc-notifications popping up unexpectedly.

Starting the service with StartCommandResult.Sticky or .NonSticky does not make any difference. Official reference is hard to follow, but this guide summarizes it better; so the latter should be used in my case (my service is internal; thus no app -> no svc).

P.S. md5 is (I guess) an MD5 hash of the package name; shortened to keep text readable.

P.P.S. Huh, it's been asked before (in 2014!), and the docs are still a mystery :((..

Astrogator
  • 1,041
  • 11
  • 27