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
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.
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( ) );
}