using DodoSSH.Client.Terminal;
namespace DodoSSH.Client.Android.Platform;
///
/// Keeps in step with what is actually running.
///
///
///
/// The service is started and stopped from one place, and that place is a count rather than a lifecycle.
/// Anything else drifts: a service started when a shell opens and stopped when a tab closes would leave
/// the notification up after the last shell died on its own, and a phone showing "1 shell connected" over
/// nothing is the same dishonesty the unlock screen's shell count exists to avoid.
///
///
/// is deliberately the source of truth rather than a
/// tally kept here. It already knows that a session whose shell exited half an hour ago is not live, which
/// a counter incremented on open and decremented on close would not.
///
///
/// Three facts feed , not one: live shells, moving
/// transfers, and an idle-but-connected Files session. The last of those used to be missing entirely —
/// a shell survives backgrounding because somebody opened it, but a Files connection with nothing moving
/// looked, to this class, exactly like nothing being open at all. holdsFileSession below is that
/// gap closed, read the same way the other two facts are: asked, not cached.
///
///
internal sealed class SessionKeepAlive : IDisposable
{
private readonly TerminalWorkspace workspace;
private readonly Func activeTransfers;
private readonly Func holdsFileSession;
/// The live shells.
///
/// How many transfers are moving bytes. A delegate rather than a queue, because ownership of the
/// transfer queue stays with TransfersViewModel — this class only ever asks it a question.
///
///
/// Whether the Files screen holds a live SFTP connection with nothing moving on it — the idle-but-
/// connected case a transfer count alone would miss. See TransfersViewModel.HasLiveFileSession.
///
public SessionKeepAlive(TerminalWorkspace workspace, Func activeTransfers, Func holdsFileSession)
{
this.workspace = workspace;
this.activeTransfers = activeTransfers;
this.holdsFileSession = holdsFileSession;
// Raised on whatever thread the workspace announced from — a continuation of the ended run, or the
// closer's own — which is fine: starting and stopping a service is a binder call and needs no
// particular thread. Nothing here touches the interface. That the announcement waits for the run to
// actually complete, and comes for deliberate closes too, is what makes reading LiveSessionCount
// from it honest — the event's own remark carries the stuck notification that taught us both.
workspace.SessionEnded += OnSessionEnded;
}
/// Re-reads the counts and starts or stops the service to match.
///
/// Called after anything that could change any of the three facts — opening a shell, closing a tab, a
/// transfer finishing, a Files connection opening or closing. Calling it when nothing changed is free:
/// reconciling to the state it is already in is either a redundant startForegroundService — or,
/// now, a redundant notification post — on a running service, or a stopService on a stopped one,
/// and Android treats all of those as no-ops.
///
public void Refresh() =>
SessionForegroundService.Reconcile(workspace.LiveSessionCount, activeTransfers(), holdsFileSession());
///
public void Dispose()
{
workspace.SessionEnded -= OnSessionEnded;
// The notification goes with the composition root. Leaving it up over a process that is shutting
// down is how an SSH client acquires a reputation for a notification you cannot get rid of.
SessionForegroundService.Reconcile(0, 0, false);
}
private void OnSessionEnded(object? sender, TerminalSessionEndedEventArgs e) => Refresh();
}